import { buildAgentDoctorArgs, buildAgentDoctorContractArgs } from './agent-doctor.mjs'; import { buildAbsentReport, buildSkippedReport, hasLegacyFixtureRepoMount, parseDockerInspectContainer, summarizeDockerMounts } from './cleanup-docker-fixtures.mjs'; import { fetchJsonWithTimeout, parseJsonPayload, pickFailureOutput, runCommand } from './command-center-utils.mjs'; import { assertDeploymentImageIdentity, assertComposeImageAutoCleanupDeploymentAllowed, assertImageAutoCleanupDeploymentAllowed, assertLocalProbeMatchesMode, buildDeploymentImageReference, buildDeploymentImageTag, buildDockerComposeArgs, buildDockerComposeEnv, buildLocalBaseUrl, isImageAutoCleanupEnabled, parsePublishedContainerPortBindings, readImageAutoCleanupValueFromComposeEnvFile, waitForLocalEndpoints } from './deploy-local.mjs'; import { buildFirstRunReport, formatFirstRunText } from './first-run.mjs'; import { buildAdminCommands, buildImageUpstreamLocalEndpointStatus, buildImageUpstreamRealSmokeStatus, parseGitStatusEntries, readStatusEnvFromFiles, readRemoteStatusFromResult } from './status.mjs'; import { buildVerifyPlan } from './verify.mjs'; import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { existsSync } from 'node:fs'; import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; import { createServer } from 'node:http'; import os from 'node:os'; import path from 'node:path'; import { describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; const deployLocalScriptPath = fileURLToPath(new URL('./deploy-local.mjs', import.meta.url)); describe('Command center scripts', () => { it('exposes a small stable administrator command set', () => { assert.deepEqual(buildAdminCommands(), { first_run: 'npm run first-run', doctor: 'npm run doctor', status: 'npm run status', env_summary: 'npm run env:summary', verify: 'npm run verify', deploy_local: 'npm run deploy:local', deploy_space: 'npm run deploy:space', docker_cleanup_fixtures: 'npm run docker:cleanup-fixtures', agent_doctor: 'npm run agent:doctor', hf_space_doctor: 'npm run doctor:hf-space', hf_space_local_smoke: 'npm run smoke:hf-space-local', hf_space_smoke_legacy_alias: 'npm run smoke:hf-space' }); }); it('keeps the full verification gate aligned with repository policy', () => { assert.deepEqual( buildVerifyPlan().map((step) => step.name), [ 'version:check', 'install-scripts:check', 'npm-install-policy:check', 'dependencies:check', 'test', 'lint', 'format:check', 'lint:scripts', 'build', 'diff-check', 'diff-cached-check' ] ); }); it('supports a quick verification loop without hiding the full gate', () => { assert.deepEqual( buildVerifyPlan({ quick: true }).map((step) => step.name), [ 'version:check', 'install-scripts:check', 'npm-install-policy:check', 'dependencies:check', 'test:scripts', 'lint:scripts', 'diff-check', 'diff-cached-check' ] ); assert.deepEqual( buildVerifyPlan({ skipBuild: true }).map((step) => step.name), [ 'version:check', 'install-scripts:check', 'npm-install-policy:check', 'dependencies:check', 'test', 'lint', 'format:check', 'lint:scripts', 'diff-check', 'diff-cached-check' ] ); }); it('can include the live PostgreSQL gate before the final diff check', () => { assert.deepEqual( buildVerifyPlan({ postgres: true }).map((step) => step.name), [ 'version:check', 'install-scripts:check', 'npm-install-policy:check', 'dependencies:check', 'test', 'lint', 'format:check', 'lint:scripts', 'build', 'test:postgres', 'diff-check', 'diff-cached-check' ] ); assert.deepEqual( buildVerifyPlan({ quick: true, postgres: true }).map((step) => step.name), [ 'version:check', 'install-scripts:check', 'npm-install-policy:check', 'dependencies:check', 'test:scripts', 'lint:scripts', 'test:postgres', 'diff-check', 'diff-cached-check' ] ); }); it('parses NUL-delimited git porcelain paths without rewriting filenames', () => { const output = [ ' M README.md', '?? path with spaces.md', 'R new name.md', 'old name.md', ' M path -> not rename.md', ' M line\nbreak.md', '' ].join('\0'); assert.deepEqual(parseGitStatusEntries(output), [ 'README.md', 'path with spaces.md', 'new name.md', 'path -> not rename.md', 'line\nbreak.md' ]); }); it('summarizes independent image upstream smoke readiness without exposing credentials', () => { const missing = buildImageUpstreamRealSmokeStatus({}); assert.equal(missing.configuration_complete, false); assert.equal(missing.configured_count, 0); assert.deepEqual(missing.missing_cases, [ 'original-images-json', 'gaoren-images-sse', 'sub2api-images-sse', 'sub2api-responses-json', 'gpt2image-responses-sse', 'matsca-images-sse' ]); assert.deepEqual(missing.request_modes['images-non-stream'], { required_count: 1, required_cases: ['original-images-json'], configuration_complete: false, configured_count: 0, configured_cases: [], missing_count: 1, missing_cases: ['original-images-json'], invalid_count: 0, invalid_cases: [], smoke_state: 'not_run_by_status' }); assert.deepEqual(missing.request_modes['responses-sse'].required_cases, ['gpt2image-responses-sse']); assert.deepEqual(missing.missing_env_any['sub2api-responses-json'][0], [ 'IMAGE_REAL_SMOKE_SUB2API_RESPONSES_BASE_URL', 'IMAGE_REAL_SMOKE_SUB2API_BASE_URL' ]); assert.equal( missing.final_gate_command, 'npm run smoke:image-upstream-real -- --env-file-if-exists .env.real-smoke.local --require-independent-targets --allow-billable' ); const configured = buildImageUpstreamRealSmokeStatus({ IMAGE_REAL_SMOKE_ORIGINAL_BASE_URL: 'https://original.example/v1', IMAGE_REAL_SMOKE_ORIGINAL_API_KEY: 'original-secret', IMAGE_REAL_SMOKE_GAOREN_BASE_URL: 'https://gaoren.example/v1', IMAGE_REAL_SMOKE_GAOREN_API_KEY: 'gaoren-secret', IMAGE_REAL_SMOKE_SUB2API_BASE_URL: 'https://sub2api.example/v1', IMAGE_REAL_SMOKE_SUB2API_API_KEY: 'sub2api-secret', IMAGE_REAL_SMOKE_SUB2API_RESPONSES_RESPONSES_MODEL: 'gpt-4.1', IMAGE_REAL_SMOKE_GPT2IMAGE_BASE_URL: 'https://gpt2image.example/v1', IMAGE_REAL_SMOKE_GPT2IMAGE_API_KEY: 'gpt2image-secret', IMAGE_REAL_SMOKE_GPT2IMAGE_RESPONSES_MODEL: 'gpt-5.4', IMAGE_REAL_SMOKE_MATSCA_BASE_URL: 'https://matsca.example/v1', IMAGE_REAL_SMOKE_MATSCA_API_KEY: 'matsca-secret' }); assert.equal(configured.configuration_complete, true); assert.equal(configured.configured_count, 6); assert.equal(configured.missing_count, 0); assert.equal(configured.request_modes['images-sse'].configured_count, 3); assert.deepEqual(configured.request_modes['responses-non-stream'].configured_cases, ['sub2api-responses-json']); assert.deepEqual(configured.request_modes['responses-sse'].configured_cases, ['gpt2image-responses-sse']); assert.doesNotMatch(JSON.stringify(configured), /secret|example\/v1/); }); it('requires Responses top-level models in image upstream status readiness', () => { const status = buildImageUpstreamRealSmokeStatus({ IMAGE_REAL_SMOKE_ORIGINAL_BASE_URL: 'https://original.example/v1', IMAGE_REAL_SMOKE_ORIGINAL_API_KEY: 'original-secret', IMAGE_REAL_SMOKE_GAOREN_BASE_URL: 'https://gaoren.example/v1', IMAGE_REAL_SMOKE_GAOREN_API_KEY: 'gaoren-secret', IMAGE_REAL_SMOKE_SUB2API_BASE_URL: 'https://sub2api.example/v1', IMAGE_REAL_SMOKE_SUB2API_API_KEY: 'sub2api-secret', IMAGE_REAL_SMOKE_GPT2IMAGE_BASE_URL: 'https://gpt2image.example/v1', IMAGE_REAL_SMOKE_GPT2IMAGE_API_KEY: 'gpt2image-secret', IMAGE_REAL_SMOKE_MATSCA_BASE_URL: 'https://matsca.example/v1', IMAGE_REAL_SMOKE_MATSCA_API_KEY: 'matsca-secret' }); assert.equal(status.configuration_complete, false); assert.equal(status.configured_count, 4); assert.deepEqual(status.configured_cases, [ 'original-images-json', 'gaoren-images-sse', 'sub2api-images-sse', 'matsca-images-sse' ]); assert.deepEqual(status.missing_cases, ['sub2api-responses-json', 'gpt2image-responses-sse']); assert.deepEqual(status.missing_env_any['sub2api-responses-json'][0], [ 'IMAGE_REAL_SMOKE_SUB2API_RESPONSES_RESPONSES_MODEL', 'OPENAI_RESPONSES_API_MODEL' ]); assert.deepEqual(status.missing_env_any['gpt2image-responses-sse'][0], [ 'IMAGE_REAL_SMOKE_GPT2IMAGE_RESPONSES_MODEL', 'OPENAI_RESPONSES_API_MODEL' ]); assert.doesNotMatch(JSON.stringify(status), /secret|example\/v1/); }); it('does not treat the sub2api Responses image model as the Responses top-level model', () => { const status = buildImageUpstreamRealSmokeStatus({ IMAGE_REAL_SMOKE_SUB2API_RESPONSES_BASE_URL: 'https://sub2api-responses.example/v1', IMAGE_REAL_SMOKE_SUB2API_RESPONSES_API_KEY: 'sub2api-responses-secret', IMAGE_REAL_SMOKE_SUB2API_RESPONSES_MODEL: 'gpt-image-2' }); assert.equal(status.configuration_complete, false); assert.equal(status.configured_count, 0); assert.deepEqual(status.missing_cases, [ 'original-images-json', 'gaoren-images-sse', 'sub2api-images-sse', 'sub2api-responses-json', 'gpt2image-responses-sse', 'matsca-images-sse' ]); assert.deepEqual(status.missing_env_any['sub2api-responses-json'][0], [ 'IMAGE_REAL_SMOKE_SUB2API_RESPONSES_RESPONSES_MODEL', 'OPENAI_RESPONSES_API_MODEL' ]); assert.doesNotMatch(JSON.stringify(status), /sub2api-responses-secret|sub2api-responses\.example/); }); it('reports unsafe independent image upstream base URLs without exposing values', () => { const status = buildImageUpstreamRealSmokeStatus({ IMAGE_REAL_SMOKE_ORIGINAL_BASE_URL: 'https://user:pass@original.example/v1?token=secret#frag', IMAGE_REAL_SMOKE_ORIGINAL_API_KEY: 'original-secret' }); assert.equal(status.configuration_complete, false); assert.equal(status.configured_count, 0); assert.equal(status.missing_count, 5); assert.equal(status.invalid_count, 1); assert.deepEqual(status.invalid_cases, ['original-images-json']); assert.deepEqual(status.invalid_env['original-images-json'], [ { key: 'IMAGE_REAL_SMOKE_ORIGINAL_BASE_URL', reason: 'must_not_include_credentials' } ]); assert.equal(status.missing_env_any['original-images-json'], undefined); assert.doesNotMatch(JSON.stringify(status), /user:pass|original\.example|token=secret|original-secret/); }); it('checks local real smoke endpoints without exposing URL paths or credentials', async () => { const status = await buildImageUpstreamLocalEndpointStatus( { IMAGE_REAL_SMOKE_ORIGINAL_BASE_URL: 'http://127.0.0.1:3010/v1?ignored=no', IMAGE_REAL_SMOKE_ORIGINAL_API_KEY: 'original-secret', IMAGE_REAL_SMOKE_GAOREN_BASE_URL: 'http://localhost:3001/v1', IMAGE_REAL_SMOKE_GAOREN_API_KEY: 'gaoren-secret', IMAGE_REAL_SMOKE_SUB2API_BASE_URL: 'https://sub2api.example/v1', IMAGE_REAL_SMOKE_SUB2API_API_KEY: 'sub2api-secret' }, { probe: async (endpoint) => ({ ok: endpoint.port === 3001, reason: endpoint.port === 3001 ? undefined : 'connection_refused' }) } ); assert.deepEqual(status, { checked_count: 1, unavailable_count: 0, unavailable_cases: [], results: [ { id: 'gaoren-images-sse', endpoint: 'localhost:3001', ok: true } ] }); assert.doesNotMatch(JSON.stringify(status), /secret|\/v1|sub2api\.example|ignored=no/); }); it('reports unavailable local real smoke endpoints by case id', async () => { const status = await buildImageUpstreamLocalEndpointStatus( { IMAGE_REAL_SMOKE_SUB2API_BASE_URL: 'http://127.0.0.1:3021/v1', IMAGE_REAL_SMOKE_SUB2API_API_KEY: 'sub2api-secret', IMAGE_REAL_SMOKE_MATSCA_BASE_URL: 'http://[::1]:3090/v1', IMAGE_REAL_SMOKE_MATSCA_API_KEY: 'matsca-secret' }, { probe: async (endpoint) => ({ ok: false, reason: endpoint.port === 3021 ? 'connection_refused' : 'timeout' }) } ); assert.deepEqual(status, { checked_count: 3, unavailable_count: 3, unavailable_cases: ['sub2api-images-sse', 'sub2api-responses-json', 'matsca-images-sse'], results: [ { id: 'sub2api-images-sse', endpoint: '127.0.0.1:3021', ok: false, reason: 'connection_refused' }, { id: 'sub2api-responses-json', endpoint: '127.0.0.1:3021', ok: false, reason: 'connection_refused' }, { id: 'matsca-images-sse', endpoint: '[::1]:3090', ok: false, reason: 'timeout' } ] }); assert.doesNotMatch(JSON.stringify(status), /secret|\/v1/); }); it('loads independent image upstream smoke readiness from env files without overriding shell env', async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), 'image-upstream-status-')); const localEnvPath = path.join(tempDir, '.env.local'); const realSmokeEnvPath = path.join(tempDir, '.env.real-smoke.local'); await writeFile( localEnvPath, [ 'IMAGE_REAL_SMOKE_ORIGINAL_BASE_URL=https://local-original.example/v1', 'IMAGE_REAL_SMOKE_ORIGINAL_API_KEY=local-original-secret', 'IMAGE_REAL_SMOKE_GAOREN_BASE_URL=https://local-gaoren.example/v1', 'IMAGE_REAL_SMOKE_GAOREN_API_KEY=local-gaoren-secret' ].join('\n') ); await writeFile( realSmokeEnvPath, [ 'IMAGE_REAL_SMOKE_GAOREN_BASE_URL=https://real-gaoren.example/v1', 'IMAGE_REAL_SMOKE_GAOREN_API_KEY=real-gaoren-secret', 'IMAGE_REAL_SMOKE_SUB2API_BASE_URL=https://real-sub2api.example/v1', 'IMAGE_REAL_SMOKE_SUB2API_API_KEY=real-sub2api-secret', 'IMAGE_REAL_SMOKE_SUB2API_RESPONSES_RESPONSES_MODEL=gpt-4.1', 'IMAGE_REAL_SMOKE_GPT2IMAGE_BASE_URL=https://real-gpt2image.example/v1', 'IMAGE_REAL_SMOKE_GPT2IMAGE_API_KEY=real-gpt2image-secret', 'IMAGE_REAL_SMOKE_GPT2IMAGE_RESPONSES_MODEL=gpt-5.4', 'IMAGE_REAL_SMOKE_MATSCA_BASE_URL=https://real-matsca.example/v1', 'IMAGE_REAL_SMOKE_MATSCA_API_KEY=real-matsca-secret' ].join('\n') ); try { const statusEnv = readStatusEnvFromFiles( { IMAGE_REAL_SMOKE_ORIGINAL_API_KEY: 'shell-original-secret' }, [ { path: localEnvPath, override: false }, { path: realSmokeEnvPath, override: true } ] ); const status = buildImageUpstreamRealSmokeStatus(statusEnv); assert.equal(statusEnv.IMAGE_REAL_SMOKE_ORIGINAL_API_KEY, 'shell-original-secret'); assert.equal(status.configuration_complete, true); assert.deepEqual(status.configured_cases, [ 'original-images-json', 'gaoren-images-sse', 'sub2api-images-sse', 'sub2api-responses-json', 'gpt2image-responses-sse', 'matsca-images-sse' ]); assert.doesNotMatch(JSON.stringify(status), /secret|example\/v1/); } finally { await rm(tempDir, { recursive: true, force: true }); } }); it('preserves mismatched env quotes in status env files', async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), 'image-upstream-status-')); const envPath = path.join(tempDir, '.env.local'); await writeFile( envPath, [ 'MATCHED_DOUBLE="https://quoted.example/v1"', "MATCHED_SINGLE='single-secret'", 'MATCHED_DOUBLE_SPACES=" kept value "', 'MISMATCHED_LEADING="kept-value', 'MISMATCHED_TRAILING=kept-value"', 'MISMATCHED_PAIR="kept-value\'', 'SINGLE_DOUBLE_QUOTE="', "SINGLE_SINGLE_QUOTE='" ].join('\n') ); try { const statusEnv = readStatusEnvFromFiles({}, [{ path: envPath, override: false }]); assert.equal(statusEnv.MATCHED_DOUBLE, 'https://quoted.example/v1'); assert.equal(statusEnv.MATCHED_SINGLE, 'single-secret'); assert.equal(statusEnv.MATCHED_DOUBLE_SPACES, ' kept value '); assert.equal(statusEnv.MISMATCHED_LEADING, '"kept-value'); assert.equal(statusEnv.MISMATCHED_TRAILING, 'kept-value"'); assert.equal(statusEnv.MISMATCHED_PAIR, '"kept-value\''); assert.equal(statusEnv.SINGLE_DOUBLE_QUOTE, '"'); assert.equal(statusEnv.SINGLE_SINGLE_QUOTE, "'"); } finally { await rm(tempDir, { recursive: true, force: true }); } }); it('builds deterministic local deploy compose arguments', () => { assert.deepEqual(buildDockerComposeArgs(), [ 'compose', '-f', 'docker-compose.yml', 'up', '-d', '--build', '--force-recreate', '--remove-orphans', '--wait', '--wait-timeout', '120' ]); assert.deepEqual(buildDockerComposeArgs({ memory: true }), [ 'compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.memory.yml', 'up', '-d', '--build', '--force-recreate', '--remove-orphans', '--wait', '--wait-timeout', '120' ]); assert.deepEqual(buildDockerComposeArgs({ postgres: true }), [ 'compose', '-f', 'docker-compose.yml', '-f', 'docker-compose.postgres.yml', 'up', '-d', '--build', '--force-recreate', '--remove-orphans', '--wait', '--wait-timeout', '120' ]); assert.throws(() => buildDockerComposeArgs({ memory: true, postgres: true }), /不能同时使用/); }); it('uses plain compose progress for diagnosable local deploy output', () => { assert.deepEqual(buildDockerComposeEnv({ PATH: '/bin', COMPOSE_PROGRESS: 'auto' }), { PATH: '/bin', COMPOSE_PROGRESS: 'plain' }); assert.deepEqual( buildDockerComposeEnv( { PATH: '/bin' }, { revision: '0123456789abcdef0123456789abcdef01234567', imageTag: 'local-0123456789abcdef0123456789abcdef01234567' } ), { PATH: '/bin', COMPOSE_PROGRESS: 'plain', GIP_IMAGE_REVISION: '0123456789abcdef0123456789abcdef01234567', GIP_IMAGE_TAG: 'local-0123456789abcdef0123456789abcdef01234567' } ); }); it('requires explicit confirmation before deploying with automatic WebUI image cleanup enabled', () => { assert.equal(isImageAutoCleanupEnabled('true'), true); assert.equal(isImageAutoCleanupEnabled('yes'), true); assert.equal(isImageAutoCleanupEnabled('false'), false); assert.equal(isImageAutoCleanupEnabled(undefined), false); assert.doesNotThrow(() => assertImageAutoCleanupDeploymentAllowed({})); assert.throws( () => assertImageAutoCleanupDeploymentAllowed({ WEBUI_IMAGE_AUTO_CLEANUP_ENABLED: 'true' }), /--allow-image-auto-cleanup/ ); assert.doesNotThrow(() => assertImageAutoCleanupDeploymentAllowed( { WEBUI_IMAGE_AUTO_CLEANUP_ENABLED: 'true' }, { allowImageAutoCleanup: true } ) ); }); it('reads the automatic cleanup setting from the Compose env file instead of the shell environment', async () => { const temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), 'deploy-local-cleanup-')); try { await writeFile( path.join(temporaryDirectory, '.env.local'), 'WEBUI_IMAGE_AUTO_CLEANUP_ENABLED=true\nWEBUI_IMAGE_RETENTION_DAYS=30\n' ); assert.equal(readImageAutoCleanupValueFromComposeEnvFile(temporaryDirectory), 'true'); assert.throws( () => assertComposeImageAutoCleanupDeploymentAllowed({}, temporaryDirectory), /--allow-image-auto-cleanup/ ); assert.doesNotThrow(() => assertComposeImageAutoCleanupDeploymentAllowed({ allowImageAutoCleanup: true }, temporaryDirectory) ); } finally { await rm(temporaryDirectory, { recursive: true, force: true }); } }); it('blocks the deploy CLI before Docker runs unless automatic cleanup is explicitly confirmed', async () => { const temporaryDirectory = await mkdtemp(path.join(os.tmpdir(), 'deploy-local-cleanup-cli-')); const fakeBinDirectory = path.join(temporaryDirectory, 'bin'); const dockerCallLog = path.join(temporaryDirectory, 'docker-calls.log'); await mkdir(fakeBinDirectory); await writeFile( path.join(temporaryDirectory, '.env.local'), 'WEBUI_IMAGE_AUTO_CLEANUP_ENABLED=true\n' ); await writeExecutable( path.join(fakeBinDirectory, 'git'), `#!/bin/sh if [ "$1" = "rev-parse" ]; then printf '%s\\n' '0123456789abcdef0123456789abcdef01234567' exit 0 fi if [ "$1" = "status" ]; then exit 0 fi exit 1 ` ); await writeExecutable( path.join(fakeBinDirectory, 'docker'), `#!/bin/sh printf '%s\\n' "$*" >> "$DEPLOY_LOCAL_DOCKER_CALL_LOG" if [ "$1" = "compose" ]; then exit 0 fi if [ "$1" = "inspect" ]; then case "$3" in *Config.Image*) printf '%s\\n' 'gpt-image-playground-customer:local-0123456789abcdef0123456789abcdef01234567' ;; *NetworkSettings.Ports*) printf '%s\\n' '[{"HostIp":"127.0.0.1","HostPort":"4783"}]' ;; *) exit 1 ;; esac exit 0 fi if [ "$1" = "image" ] && [ "$2" = "inspect" ]; then printf '%s\\n' '0123456789abcdef0123456789abcdef01234567' exit 0 fi exit 1 ` ); try { const baseEnv = { PATH: `${fakeBinDirectory}${path.delimiter}${process.env.PATH || ''}`, DEPLOY_LOCAL_DOCKER_CALL_LOG: dockerCallLog }; const blocked = await runNodeCommandAsync([deployLocalScriptPath, '--skip-probe'], { cwd: temporaryDirectory, env: baseEnv }); assert.equal(blocked.status, 1); assert.match(blocked.stdout, /--allow-image-auto-cleanup/); assert.equal(existsSync(dockerCallLog), false); const confirmed = await runNodeCommandAsync( [deployLocalScriptPath, '--allow-image-auto-cleanup', '--skip-probe'], { cwd: temporaryDirectory, env: baseEnv } ); assert.equal(confirmed.status, 0); assert.equal(parseJsonPayload(confirmed.stdout).ok, true); assert.match(await readFile(dockerCallLog, 'utf8'), /^compose /m); } finally { await rm(temporaryDirectory, { recursive: true, force: true }); } }); it('uses immutable full-revision Docker image tags for deploys', () => { const revision = '0123456789abcdef0123456789abcdef01234567'; assert.equal(buildDeploymentImageTag(revision), `local-${revision}`); assert.equal(buildDeploymentImageReference(revision), `gpt-image-playground-customer:local-${revision}`); assert.throws(() => buildDeploymentImageTag('0123456'), /40/); assert.throws(() => buildDeploymentImageTag('z'.repeat(40)), /40/); }); it('builds local probe URLs from the published container port mapping', () => { assert.equal(buildLocalBaseUrl('127.0.0.1', '4783'), 'http://127.0.0.1:4783'); assert.equal(buildLocalBaseUrl('0.0.0.0', '4784'), 'http://127.0.0.1:4784'); assert.equal(buildLocalBaseUrl('::1', '4783'), 'http://[::1]:4783'); assert.deepEqual(parsePublishedContainerPortBindings('[{"HostIp":"127.0.0.1","HostPort":"4783"}]'), { bindHost: '127.0.0.1', hostPort: '4783', baseUrl: 'http://127.0.0.1:4783' }); assert.deepEqual( parsePublishedContainerPortBindings('[{"HostIp":"::","HostPort":"4784"},{"HostIp":"0.0.0.0","HostPort":"4784"}]'), { bindHost: '0.0.0.0', hostPort: '4784', baseUrl: 'http://127.0.0.1:4784' } ); assert.throws(() => buildLocalBaseUrl('127.0.0.1', '0'), /1 到 65535/); assert.throws(() => parsePublishedContainerPortBindings('not-json'), /端口映射/); assert.throws(() => parsePublishedContainerPortBindings('null'), /未发布/); }); it('waits only between failed local endpoint probe attempts', async () => { let requestCount = 0; let sleepCount = 0; await assert.rejects( waitForLocalEndpoints('http://127.0.0.1:4783', { attempts: 2, intervalMs: 2000, fetchJson: async () => { requestCount += 1; throw new Error('container not ready'); }, sleep: async () => { sleepCount += 1; } }), /container not ready/ ); assert.equal(requestCount, 2); assert.equal(sleepCount, 1); }); it('requires the running Docker image and revision label to match the deployed revision', () => { const deployment = { revision: '0123456789abcdef0123456789abcdef01234567', imageTag: 'local-0123456789abcdef0123456789abcdef01234567' }; assert.doesNotThrow(() => assertDeploymentImageIdentity( { image: 'gpt-image-playground-customer:local-0123456789abcdef0123456789abcdef01234567', revision: deployment.revision }, deployment ) ); assert.throws( () => assertDeploymentImageIdentity( { image: 'gpt-image-playground-customer:local', revision: deployment.revision }, deployment ), /镜像不匹配/ ); assert.throws( () => assertDeploymentImageIdentity( { image: 'gpt-image-playground-customer:local-0123456789abcdef0123456789abcdef01234567', revision: 'different' }, deployment ), /revision 不匹配/ ); }); it('detects only the legacy fixture whole-repository Docker mount', () => { const repoRoot = '/Volumes/Work/code/gpt-image-playground-customer'; assert.equal( hasLegacyFixtureRepoMount( [ { Type: 'bind', Source: repoRoot, Destination: '/workspace', Mode: 'ro', RW: false } ], repoRoot ), true ); assert.equal( hasLegacyFixtureRepoMount( [ { Type: 'bind', Source: `${repoRoot}/generated-images`, Destination: '/app/generated-images', Mode: '', RW: true } ], repoRoot ), false ); }); it('parses and summarizes Docker fixture cleanup inspection data', () => { const container = parseDockerInspectContainer( JSON.stringify([ { Mounts: [ { Type: 'bind', Source: '/repo', Destination: '/workspace', Mode: 'ro', RW: false } ] } ]) ); assert.deepEqual(summarizeDockerMounts(container.Mounts), [ { type: 'bind', source: '/repo', destination: '/workspace', mode: 'ro', writable: false } ]); assert.deepEqual(buildAbsentReport('gipc-local-image-fixture'), { ok: true, command: 'docker:cleanup-fixtures', container: 'gipc-local-image-fixture', present: false, unsafe_repo_mount: false, removed: false }); assert.equal(buildSkippedReport('custom', container.Mounts).removed, false); }); it('requires local deploy probes to match the selected state and storage backend', () => { assert.doesNotThrow(() => assertLocalProbeMatchesMode({ stateBackend: 'sqlite', imageStorageMode: 'fs' })); assert.throws( () => assertLocalProbeMatchesMode({ stateBackend: 'memory', imageStorageMode: 'indexeddb' }), /SQLite deployment mode did not take effect/ ); assert.doesNotThrow(() => assertLocalProbeMatchesMode({ stateBackend: 'memory', imageStorageMode: 'indexeddb' }, { memory: true }) ); assert.throws( () => assertLocalProbeMatchesMode({ stateBackend: 'sqlite', imageStorageMode: 'fs' }, { memory: true }), /Memory deployment mode did not take effect/ ); assert.doesNotThrow(() => assertLocalProbeMatchesMode({ stateBackend: 'postgres', imageStorageMode: 'fs' }, { postgres: true }) ); assert.throws( () => assertLocalProbeMatchesMode({ stateBackend: 'sqlite', imageStorageMode: 'fs' }, { postgres: true }), /PostgreSQL deployment mode did not take effect/ ); assert.throws( () => assertLocalProbeMatchesMode( { stateBackend: 'memory', imageStorageMode: 'indexeddb' }, { memory: true, postgres: true } ), /不能同时使用/ ); }); it('routes agent:doctor through the non-billable contract check', () => { const args = buildAgentDoctorArgs(); assert.match(args[0], /generate-image\.mjs$/); assert.deepEqual(args.slice(1), ['--contract-check', '--timeout-ms', '60000', 'contract check']); }); it('routes agent:doctor contract check through an explicit base URL', () => { const args = buildAgentDoctorContractArgs('https://space.example.test/'); assert.match(args[0], /generate-image\.mjs$/); assert.deepEqual(args.slice(1), [ '--contract-check', '--timeout-ms', '60000', '--base-url', 'https://space.example.test/', 'contract check' ]); }); it('reports first-run readiness without exposing secrets', async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), 'first-run-')); await writeFile(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'demo', version: '1.0.0' })); await writeFile(path.join(tempDir, 'package-lock.json'), '{}'); await writeFile(path.join(tempDir, '.env.agent.local'), 'GPT_IMAGE_AGENT_TOKEN=file-secret\n'); await withServer( (request, response) => { if (request.url === '/api/agent/capabilities') { assert.equal(request.headers.authorization, 'Bearer shell-secret'); response.writeHead(200, { 'content-type': 'application/json' }); response.end( JSON.stringify({ auth: { schemes: ['bearer'] }, defaults: { state_backend: 'memory' }, agent_streaming: { page_sse: { supported: true, auth: { required: true, form_field: 'passwordHash' } } }, supported: { image_backend_requirements: {} } }) ); return; } if (request.url === '/api/runtime-capabilities') { response.writeHead(200, { 'content-type': 'application/json' }); response.end(JSON.stringify({ streaming: { defaultMode: 'auto' } })); return; } response.writeHead(404, { 'content-type': 'application/json' }); response.end(JSON.stringify({ error: 'missing' })); }, async (baseUrl) => { try { const report = await buildFirstRunReport( { cwd: tempDir, baseUrl, envFiles: ['.env.agent.local'], timeoutMs: 1000 }, { GPT_IMAGE_AGENT_TOKEN: 'shell-secret' } ); assert.equal(report.command, 'first-run'); assert.equal(report.billable, false); assert.equal(report.service_base_url, baseUrl); assert.equal(report.service_base_url_source, 'user_provided'); assert.equal(report.interactive_confirmation_required, false); assert.deepEqual(report.agent_auth_process, { has_token: true, has_password_hash: false, has_any_auth: true }); assert.deepEqual(report.agent_auth_service, { required: true, required_schemes: ['bearer'], ready: true, has_token: true, has_password_hash: false, has_any_auth: true }); assert.deepEqual(report.private_agent_env, { exists: true, has_token: true, has_password_hash: false }); assert.equal(report.service.ok, true); assert.equal( report.checks.find((check) => check.name === 'dependencies_installed').hint, '运行 npm run install-scripts:check && npm run npm-install-policy:check && npm ci --strict-allow-scripts && npm run dependencies:check。' ); assert.equal( report.checks.find((check) => check.name === 'agent_auth_available_to_process').ok, true ); assert.equal( report.checks.find((check) => check.name === 'page_sse_auth_available_to_process').ok, false ); assert.equal( report.checks.find((check) => check.name === 'page_sse_auth_available_to_process').skipped, false ); assert.equal( report.checks.find((check) => check.name === 'page_sse_auth_available_to_process') .auth_in_private_env_file, false ); assert.equal(report.service.capabilities.page_sse_auth_required, true); assert.equal(report.service.capabilities.page_sse_auth_form_field, 'passwordHash'); assert.equal(report.service.capabilities.page_sse_declared_supported, true); assert.equal(report.service.capabilities.page_sse_real_smoke, 'not_run_by_first_run'); assert.equal( report.service.capabilities.responses_image_backend_real_smoke, 'not_run_by_first_run' ); assert.deepEqual(report.service.capabilities.page_sse_real_smoke_status, { state: 'not_run', billable: false, reason: 'first-run is non-billable and does not call /api/images' }); assert.deepEqual(report.service.capabilities.responses_image_backend_real_smoke_status, { state: 'not_run', billable: false, reason: 'first-run is non-billable and does not call Responses image generation' }); assert.match(formatFirstRunText(report), /页面 SSE:声明支持,实测=未执行真实 smoke/); assert.match( formatFirstRunText(report), /Responses 后端:声明未支持,启用=否,实测=未执行真实 smoke/ ); assert.equal( report.checks.find((check) => check.name === 'agent_auth_available_to_process') .auth_in_private_env_file, true ); assert.doesNotMatch(JSON.stringify(report), /shell-secret|file-secret/); assert.match( JSON.stringify(report.next_actions), /npm run install-scripts:check && npm run npm-install-policy:check && npm ci --strict-allow-scripts && npm run dependencies:check/ ); assert.match(JSON.stringify(report.next_actions), /agent:doctor/); assert.match(JSON.stringify(report.next_actions), /GPT_IMAGE_APP_PASSWORD_HASH/); assert.match(JSON.stringify(report.next_actions), /GPT_IMAGE_AGENT_TOKEN/); } finally { await rm(tempDir, { recursive: true, force: true }); } } ); }); it('does not treat an interrupted dependency installation as ready', async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), 'first-run-incomplete-dependencies-')); await writeFile(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'demo', version: '1.0.0' })); await writeFile( path.join(tempDir, 'package-lock.json'), JSON.stringify({ lockfileVersion: 3, packages: { '': { dependencies: { demo: '1.0.0' } }, 'node_modules/demo': { version: '1.0.0' } } }) ); await mkdir(path.join(tempDir, 'node_modules')); await writeFile( path.join(tempDir, 'node_modules', '.package-lock.json'), JSON.stringify({ lockfileVersion: 3, packages: { 'node_modules/demo': { version: '1.0.0' } } }) ); await withServer( (_request, response) => { response.writeHead(200, { 'content-type': 'application/json' }); response.end(JSON.stringify({ defaults: {}, supported: {} })); }, async (baseUrl) => { try { const report = await buildFirstRunReport( { cwd: tempDir, baseUrl, envFiles: [], timeoutMs: 1000 }, {} ); const dependencies = report.checks.find((check) => check.name === 'dependencies_installed'); assert.equal(dependencies.ok, false); assert.equal(dependencies.reason, 'direct_package_missing'); assert.deepEqual(dependencies.missing_packages, ['demo']); assert.equal( dependencies.hint, '运行 npm run install-scripts:check && npm run npm-install-policy:check && npm ci --strict-allow-scripts && npm run dependencies:check。' ); } finally { await rm(tempDir, { recursive: true, force: true }); } } ); }); it('treats bearer-required services as not ready when only APP_PASSWORD is loaded', async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), 'first-run-auth-bearer-')); await writeFile(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'demo', version: '1.0.0' })); await withServer( (_request, response) => { response.writeHead(200, { 'content-type': 'application/json' }); response.end( JSON.stringify({ auth: { schemes: ['bearer'] }, defaults: { state_backend: 'memory' } }) ); }, async (baseUrl) => { try { const report = await buildFirstRunReport( { cwd: tempDir, baseUrl, envFiles: ['.env.agent.local'], timeoutMs: 1000 }, { GPT_IMAGE_APP_PASSWORD_HASH: 'shell-secret' } ); assert.deepEqual(report.agent_auth_process, { has_token: false, has_password_hash: true, has_any_auth: true }); assert.deepEqual(report.agent_auth_service, { required: true, required_schemes: ['bearer'], ready: false, has_token: false, has_password_hash: false, has_any_auth: false }); assert.match(formatFirstRunText(report), /当前进程鉴权:已加载访问码哈希/); assert.match(formatFirstRunText(report), /服务鉴权:未满足当前服务鉴权要求/); assert.match(JSON.stringify(report.next_actions), /GPT_IMAGE_AGENT_TOKEN/); assert.doesNotMatch(JSON.stringify(report.next_actions), /GPT_IMAGE_APP_PASSWORD_HASH/); } finally { await rm(tempDir, { recursive: true, force: true }); } } ); }); it('formats first-run as human-readable text by default', async () => { const report = await buildFirstRunReport( { cwd: process.cwd(), baseUrl: 'not a url', envFiles: [] }, { GPT_IMAGE_AGENT_LOAD_ENV_FILE: '0' } ); const text = formatFirstRunText(report); assert.match(text, /^首次配置检查:需要处理/m); assert.match(text, /服务地址:无效/); assert.match(text, /当前进程鉴权:未加载/); assert.match(text, /私有 Agent env:不存在/); assert.match(text, /下一步:/); assert.doesNotMatch(text, /实测=passed/); assert.doesNotMatch(text, /^\{/); }); it('preserves path-prefixed service URLs in first-run reports', async () => { // This test only checks local URL normalization in the report; .test is reserved and should not resolve. const report = await buildFirstRunReport( { cwd: process.cwd(), baseUrl: 'https://space.example.test/proxy/', envFiles: [], timeoutMs: 5 }, {} ); assert.equal(report.service_base_url, 'https://space.example.test/proxy'); }); it('prints localized first-run help and option errors', async () => { const help = await runNodeCommandAsync(['scripts/first-run.mjs', '--help'], { env: process.env, timeoutMs: 5_000 }); assert.equal(help.ok, true); assert.match(help.stdout, /用法:/); assert.match(help.stdout, /输出机器可读 JSON/); assert.doesNotMatch(help.stdout, /Usage:/); const invalid = await runNodeCommandAsync(['scripts/first-run.mjs', '--missing-option'], { env: process.env, timeoutMs: 5_000 }); assert.equal(invalid.ok, false); assert.match(invalid.stdout, /"ok": false/); assert.match(invalid.stdout, /未知参数:--missing-option/); }); it('keeps first-run local discovery explicit when no URL is provided', async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), 'first-run-default-')); try { await writeFile(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'demo', version: '1.0.0' })); const report = await buildFirstRunReport({ cwd: tempDir, envFiles: [], timeoutMs: 20 }, {}); assert.equal(report.service_base_url_source, 'default_local_probe'); assert.equal(report.interactive_confirmation_required, true); assert.equal(report.ok, false); assert.match(JSON.stringify(report.next_actions), /确认探测到的服务地址/); } finally { await rm(tempDir, { recursive: true, force: true }); } }); it('points first-run at auth setup when the service rejects capabilities with 401', async () => { const tempDir = await mkdtemp(path.join(os.tmpdir(), 'first-run-auth-')); await writeFile(path.join(tempDir, 'package.json'), JSON.stringify({ name: 'demo', version: '1.0.0' })); await withServer( (_request, response) => { response.writeHead(401, { 'content-type': 'application/json' }); response.end(JSON.stringify({ error: 'unauthorized' })); }, async (baseUrl) => { try { const report = await buildFirstRunReport( { cwd: tempDir, baseUrl: `${baseUrl}/`, envFiles: [], timeoutMs: 1000 }, {} ); assert.equal(report.service_base_url, baseUrl); assert.equal( report.checks.find((check) => check.name === 'agent_auth_available_to_process').skipped, false ); assert.equal(report.checks.find((check) => check.name === 'service_reachable').status, 401); assert.equal('runtime' in report.service, false); assert.match(JSON.stringify(report.next_actions), /GPT_IMAGE_AGENT_TOKEN/); assert.doesNotMatch(JSON.stringify(report.next_actions), /Start the service/); } finally { await rm(tempDir, { recursive: true, force: true }); } } ); }); it('reports layered agent:doctor diagnostics without billable smoke by default', async () => { await withServer( (request, response) => { if (request.url === '/proxy/api/agent/capabilities') { response.writeHead(200, { 'content-type': 'application/json' }); response.end( JSON.stringify({ defaults: { state_backend: 'memory' }, storage: { image_storage_mode: 'indexeddb', postgres_configured: false }, agent_streaming: { page_sse: { supported: true, auth: { required: true, form_field: 'passwordHash' } } }, agent_jobs: { supported: true }, orchestration: { supported: true, endpoint: '/api/agent/image-requests', transport_selection: 'server_owned' }, request_mode_controls: { global_env: 'OPENAI_UPSTREAM_REQUEST_MODES', channel_env_pattern: 'OPENAI_CHANNEL_N_REQUEST_MODES', agent_client_policy: 'diagnostics_only' }, routing_rules: { high_resolution_edit: { conditions: { operation: 'edit', max_edge: { operator: 'gt', value: 2048 } } } }, supported: { request_modes: [ 'images-non-stream', 'images-sse', 'images-json', 'responses-non-stream', 'responses-sse' ], image_backend_requirements: { 'responses-image-generation': { supported: true, enabled: true, missing_env: [] } } }, upstream_request_headers: { channels: [ { id: 'images', request_modes: ['images-non-stream', 'images-sse'] }, { id: 'responses', request_modes: ['responses-sse'] } ] } }) ); return; } if (request.url === '/proxy/api/runtime-capabilities') { response.writeHead(200, { 'content-type': 'application/json' }); response.end( JSON.stringify({ streaming: { defaultMode: 'auto', unavailableMarkScope: 'channel+backend+strategy+operation' }, streamingBatch: { enabled: true, recommendedConcurrency: 2 }, channelQueue: { enabled: true, capacityPerCredential: 1 }, responsesImageBackend: { enabled: true, mode: 'experimental' }, channelRouting: { strategy: 'round_robin', requestModeControls: { global_env: 'OPENAI_UPSTREAM_REQUEST_MODES', channel_env_pattern: 'OPENAI_CHANNEL_N_REQUEST_MODES', agent_client_policy: 'diagnostics_only' }, configuredRequestModes: ['images-non-stream', 'images-sse', 'responses-sse'], effectiveRequestModes: ['images-non-stream', 'images-sse'], effectiveRequestModesByChannel: [ { channelId: 'images', requestModes: ['images-non-stream', 'images-sse'] } ] } }) ); return; } if (request.url === '/proxy/api/agent/images/generate') { if (request.headers['idempotency-key']) { response.writeHead(200, { 'content-type': 'application/json' }); response.end(JSON.stringify({ images: [{ id: 'doctor-generate', filename: 'doctor.png' }] })); return; } response.writeHead(400, { 'content-type': 'application/json' }); response.end( JSON.stringify({ error: { code: 'idempotency_key_required', message: 'missing key', retryable: false } }) ); return; } if (request.url === '/proxy/api/agent/image-requests') { response.writeHead(400, { 'content-type': 'application/json' }); response.end( JSON.stringify({ error: { code: 'idempotency_key_required', message: 'missing key', retryable: false } }) ); return; } if (request.url === '/proxy/api/agent/jobs/images/generate') { response.writeHead(400, { 'content-type': 'application/json' }); response.end( JSON.stringify({ error: { code: 'idempotency_key_required', message: 'missing key', retryable: false } }) ); return; } response.writeHead(404, { 'content-type': 'application/json' }); response.end(JSON.stringify({ error: 'missing' })); }, async (baseUrl) => { const prefixedBaseUrl = `${baseUrl}/proxy`; const result = await runNodeCommandAsync(['scripts/agent-doctor.mjs'], { env: { ...process.env, GPT_IMAGE_PLAYGROUND_URL: `${prefixedBaseUrl}/` }, timeoutMs: 15_000 }); assert.equal(result.ok, true); const body = parseJsonPayload(result.stdout, 'agent doctor'); assert.equal(body.ok, true); assert.equal(body.billable, false); assert.equal(body.summary.capabilities, 'ok'); assert.equal(body.summary.runtime, 'ok'); assert.equal(body.summary.state_backend, 'memory'); assert.equal(body.summary.page_sse_auth_ready, false); assert.equal(body.summary.page_sse_declared_supported, true); assert.equal(body.summary.page_sse_real_smoke, 'skipped'); assert.equal(body.summary.orchestration_generate_smoke, 'skipped'); assert.equal(body.summary.agent_generate_smoke, 'skipped'); assert.equal(body.summary.responses_page_sse_generate_smoke, 'skipped'); assert.deepEqual(body.summary.real_smoke_checks, { orchestration_generate_1k: 'skipped', agent_generate_1k: 'skipped', responses_page_sse_generate_1k: 'skipped', responses_agent_generate_1k: 'skipped', agent_edit_1k: 'skipped', page_sse_edit_2k: 'skipped' }); assert.deepEqual(body.summary.request_modes.supported, [ 'images-non-stream', 'images-sse', 'images-json', 'responses-non-stream', 'responses-sse' ]); assert.deepEqual(body.summary.request_modes.configured, [ 'images-non-stream', 'images-sse', 'responses-sse' ]); assert.deepEqual(body.summary.request_modes.effective, ['images-non-stream', 'images-sse']); assert.deepEqual(body.summary.request_modes.admin_whitelist_by_channel, [ { channel_id: 'images', request_modes: ['images-non-stream', 'images-sse'] }, { channel_id: 'responses', request_modes: ['responses-sse'] } ]); assert.deepEqual(body.summary.request_modes.effective_by_channel, [ { channel_id: 'images', request_modes: ['images-non-stream', 'images-sse'] } ]); assert.deepEqual(body.summary.request_modes.gaps, [ { code: 'unrecognized_request_modes', severity: 'warning', request_modes: ['images-json'], message: '服务返回了当前 Agent 未识别的 request mode;升级 skill 或确认服务端模式名称。' }, { code: 'configured_request_modes_not_effective', severity: 'warning', request_modes: ['responses-sse'], message: '部分已配置 request mode 没有在 runtime 生效;检查对应渠道 key、健康状态和白名单。' }, { code: 'channels_without_effective_request_modes', severity: 'warning', channel_ids: ['responses'], message: '部分渠道没有生效 request mode。' } ]); assert.equal(body.summary.request_modes.suggested_channel_env_key, 'OPENAI_CHANNEL_N_REQUEST_MODES'); assert.equal(body.summary.request_modes.suggested_effective_value, 'images-non-stream,images-sse'); assert.match(body.summary.request_modes.next_action, /先修正未生效的渠道 request mode/); assert.deepEqual(body.summary.request_modes.smoke['responses-non-stream'].checks, [ 'responses_agent_generate_1k' ]); assert.deepEqual(body.summary.request_modes.smoke['images-non-stream'].checks, [ 'orchestration_generate_1k', 'edit_1k' ]); assert.equal(body.summary.request_modes.smoke['responses-non-stream'].state, 'skipped'); assert.equal(body.summary.request_modes.smoke['responses-sse'].state, 'skipped'); assert.equal(body.summary.request_modes.smoke['responses-sse'].billable, false); assert.equal(body.summary.responses_gpt2image_ready, true); assert.equal(body.summary.responses_image_backend_declared_supported, true); assert.deepEqual(body.summary.runtime_environment, { state_backend: 'memory', image_storage_mode: 'indexeddb', postgres_configured: false, page_sse_auth_required: true, agent_auth_schemes: [], orchestration_endpoint: '/api/agent/image-requests', orchestration_transport_selection: 'server_owned', request_mode_control_policy: 'diagnostics_only', request_mode_controls: { global_env: 'OPENAI_UPSTREAM_REQUEST_MODES', channel_env_pattern: 'OPENAI_CHANNEL_N_REQUEST_MODES', agent_client_policy: 'diagnostics_only' }, runtime_strategy: 'round_robin', effective_request_modes: ['images-non-stream', 'images-sse'], streaming_batch_enabled: true, recommended_concurrency: 2, channel_queue_enabled: true, channel_queue_capacity_per_credential: 1 }); assert.equal(body.summary.billable_smoke, 'skipped'); assert.equal(body.layers.find((layer) => layer.name === 'billable_smoke').skipped, true); assert.ok( body.layers .find((layer) => layer.name === 'billable_smoke') .checks.some((check) => check.name === 'responses_agent_generate_1k' && check.skipped === true) ); assert.equal(body.layers.find((layer) => layer.name === 'capabilities').executable_routing_rules, true); assert.deepEqual(body.layers.find((layer) => layer.name === 'capabilities').request_modes_supported, [ 'images-non-stream', 'images-sse', 'images-json', 'responses-non-stream', 'responses-sse' ]); assert.deepEqual( body.layers.find((layer) => layer.name === 'runtime_backend').effective_request_modes, ['images-non-stream', 'images-sse'] ); assert.equal( body.layers.find((layer) => layer.name === 'capabilities').page_sse_declared_supported, true ); assert.equal(body.layers.find((layer) => layer.name === 'capabilities').page_sse_auth_required, true); assert.equal(body.layers.find((layer) => layer.name === 'capabilities').page_sse_auth_ready, false); assert.equal( body.layers.find((layer) => layer.name === 'responses_gpt2image_readiness').declared_supported, true ); assert.match( body.layers.find((layer) => layer.name === 'responses_gpt2image_readiness').real_smoke_gate, /gpt2image-responses-sse/ ); assert.deepEqual( body.layers.find((layer) => layer.name === 'responses_gpt2image_readiness').real_smoke_gates, [ 'npm run smoke:image-upstream-real -- --env-file-if-exists .env.real-smoke.local --case sub2api-responses-json --allow-billable', 'npm run smoke:image-upstream-real -- --env-file-if-exists .env.real-smoke.local --case gpt2image-responses-sse --allow-billable' ] ); assert.match( body.layers.find((layer) => layer.name === 'capabilities').page_sse_auth_next_action, /GPT_IMAGE_APP_PASSWORD_HASH/ ); assert.equal(body.service_base_url, prefixedBaseUrl); assert.equal(body.service_base_url_source, 'GPT_IMAGE_PLAYGROUND_URL'); assert.equal(body.interactive_confirmation_required, true); } ); }); it('uses an explicit base URL for agent:doctor billable smoke checks', async () => { const hits = []; await withServer( (request, response) => { hits.push(request.url); if (request.url === '/api/agent/capabilities') { response.writeHead(200, { 'content-type': 'application/json' }); response.end( JSON.stringify({ defaults: { state_backend: 'memory' }, storage: { image_storage_mode: 'indexeddb', postgres_configured: false }, agent_streaming: { page_sse: { supported: true } }, agent_jobs: { supported: true }, orchestration: { supported: true, endpoint: '/api/agent/image-requests', transport_selection: 'server_owned' }, routing_rules: { high_resolution_edit: { conditions: { operation: 'edit', max_edge: { operator: 'gt', value: 2048 } } } }, supported: { image_backend_requirements: { 'responses-image-generation': { supported: true, enabled: true, missing_env: [] } } } }) ); return; } if (request.url === '/api/runtime-capabilities') { response.writeHead(200, { 'content-type': 'application/json' }); response.end( JSON.stringify({ streaming: { defaultMode: 'auto', unavailableMarkScope: 'channel+backend+strategy+operation' }, streamingBatch: { enabled: true }, responsesImageBackend: { enabled: true, mode: 'experimental' } }) ); return; } if (request.url === '/api/agent/images/generate') { if (request.headers['idempotency-key']) { response.writeHead(200, { 'content-type': 'application/json' }); response.end(JSON.stringify({ images: [{ id: 'doctor-generate', filename: 'doctor.png' }] })); } else { response.writeHead(400, { 'content-type': 'application/json' }); response.end( JSON.stringify({ error: { code: 'idempotency_key_required', message: 'missing key', retryable: false } }) ); } return; } if (request.url === '/api/images') { (async () => { const requestText = await readRequestText(request); if (requestText.includes('contract check')) { response.writeHead(400, { 'content-type': 'application/json' }); response.end(JSON.stringify({ error: 'clientRequestId 长度不能超过 128 个字符。' })); return; } response.writeHead(200, { 'content-type': 'text/event-stream' }); response.end( [ 'data: {"type":"completed","filename":"doctor-page-sse.png","path":"/api/image/doctor-page-sse.png","output_format":"png"}', '', 'data: {"type":"done","client_request_id":"doctor-page-sse-request","images":[{"filename":"doctor-page-sse.png"}]}', '', '' ].join('\n') ); })().catch((error) => { response.writeHead(500, { 'content-type': 'application/json' }); response.end(JSON.stringify({ error: error.message })); }); return; } if (request.url === '/api/agent/image-requests') { if (request.headers['idempotency-key']) { response.writeHead(202, { 'content-type': 'application/json' }); response.end( JSON.stringify({ job: { id: 'doctor-orchestration-job', state: 'running', result_url: '/api/agent/jobs/doctor-orchestration-job/result', retry_after_seconds: 1 } }) ); } else { response.writeHead(400, { 'content-type': 'application/json' }); response.end( JSON.stringify({ error: { code: 'idempotency_key_required', message: 'missing key', retryable: false } }) ); } return; } if (request.url === '/api/agent/jobs/doctor-orchestration-job/result') { response.writeHead(200, { 'content-type': 'application/json' }); response.end( JSON.stringify({ request_id: 'doctor-orchestration-job', idempotency_key: 'agent-doctor-orchestration-generate', cached: false, images: [{ id: 'doctor-orchestration', filename: 'doctor-orchestration.png' }], execution: { transport: 'server_orchestrated', endpoint: '/api/agent/image-requests', route_mode: 'auto', image_backend: 'images-api', stream_mode: 'non_stream', streaming_strategy: 'off', channel_request_mode: 'images-non-stream', channel_request_mode_fallback_applied: false } }) ); return; } if (request.url === '/api/agent/jobs/images/generate') { response.writeHead(202, { 'content-type': 'application/json' }); response.end( JSON.stringify({ job: { id: 'doctor-agent-job', state: 'running', result_url: '/api/agent/jobs/doctor-agent-job/result', retry_after_seconds: 1 } }) ); return; } if (request.url === '/api/agent/jobs/doctor-agent-job/result') { response.writeHead(200, { 'content-type': 'application/json' }); response.end( JSON.stringify({ request_id: 'doctor-agent-job', idempotency_key: 'agent-doctor-agent-generate', cached: false, images: [{ id: 'doctor-agent', filename: 'doctor-agent.png' }], execution: { transport: 'agent_json', endpoint: '/api/agent/jobs/images/generate', route_mode: 'agent', image_backend: 'images-api', stream_mode: 'non_stream', streaming_strategy: 'off', channel_request_mode: 'images-non-stream', channel_request_mode_fallback_applied: false } }) ); return; } response.writeHead(404, { 'content-type': 'application/json' }); response.end(JSON.stringify({ error: 'missing' })); }, async (baseUrl) => { const result = await runNodeCommandAsync( ['scripts/agent-doctor.mjs', '--base-url', baseUrl, '--allow-billable'], { timeoutMs: 15_000 } ); assert.equal(result.ok, true); const body = parseJsonPayload(result.stdout, 'agent doctor'); assert.equal(body.service_base_url_source, 'user_provided'); assert.equal(body.interactive_confirmation_required, false); assert.equal(body.summary.page_sse_real_smoke, 'passed'); assert.equal(body.summary.orchestration_generate_smoke, 'passed'); assert.equal(body.summary.agent_generate_smoke, 'passed'); assert.equal(body.summary.responses_page_sse_generate_smoke, 'passed'); assert.equal(body.summary.responses_agent_generate_smoke, 'passed'); assert.equal(body.summary.real_smoke_checks.orchestration_generate_1k, 'passed'); assert.equal(body.summary.real_smoke_checks.agent_generate_1k, 'passed'); assert.equal(body.summary.real_smoke_checks.responses_page_sse_generate_1k, 'passed'); assert.equal(body.summary.real_smoke_checks.responses_agent_generate_1k, 'passed'); assert.equal(body.summary.request_modes.smoke['images-non-stream'].state, 'passed'); assert.equal(body.summary.request_modes.smoke['responses-non-stream'].state, 'passed'); assert.equal(body.summary.request_modes.smoke['responses-sse'].state, 'passed'); assert.equal(body.summary.real_smoke_checks.agent_edit_1k, 'skipped'); assert.equal(body.summary.real_smoke_checks.page_sse_edit_2k, 'skipped'); assert.ok(hits.includes('/api/agent/images/generate')); assert.ok(hits.includes('/api/agent/image-requests')); assert.ok(hits.includes('/api/images')); } ); }); it('reports failed agent:doctor page SSE smoke checks without hiding the failure', async () => { await withServer( (request, response) => { if (request.url === '/api/agent/capabilities') { response.writeHead(200, { 'content-type': 'application/json' }); response.end( JSON.stringify({ defaults: { state_backend: 'memory' }, storage: { image_storage_mode: 'indexeddb', postgres_configured: false }, orchestration: { supported: true, endpoint: '/api/agent/image-requests', transport_selection: 'server_owned' }, agent_streaming: { page_sse: { supported: true } }, agent_jobs: { supported: true }, routing_rules: {}, supported: { request_modes: ['images-non-stream', 'responses-non-stream', 'responses-sse'], image_backend_requirements: { 'responses-image-generation': { supported: true, enabled: true, missing_env: [] } } }, upstream_request_headers: { channels: [ { id: 'images', request_modes: ['images-non-stream'] }, { id: 'responses', request_modes: ['responses-non-stream', 'responses-sse'] } ] } }) ); return; } if (request.url === '/api/runtime-capabilities') { response.writeHead(200, { 'content-type': 'application/json' }); response.end( JSON.stringify({ streaming: { defaultMode: 'auto', unavailableMarkScope: 'channel+backend+strategy+operation' }, streamingBatch: { enabled: true }, responsesImageBackend: { enabled: true, mode: 'experimental' }, channelRouting: { configuredRequestModes: ['images-non-stream', 'responses-non-stream', 'responses-sse'], effectiveRequestModes: ['images-non-stream', 'responses-non-stream', 'responses-sse'], effectiveRequestModesByChannel: [ { channelId: 'images', requestModes: ['images-non-stream'] }, { channelId: 'responses', requestModes: ['responses-non-stream', 'responses-sse'] } ] } }) ); return; } if (request.url === '/api/agent/images/generate') { response.writeHead(200, { 'content-type': 'application/json' }); response.end(JSON.stringify({ images: [{ id: 'doctor-generate', filename: 'doctor.png' }] })); return; } if (request.url === '/api/agent/image-requests') { if (request.headers['idempotency-key']) { response.writeHead(202, { 'content-type': 'application/json' }); response.end( JSON.stringify({ job: { id: 'doctor-orchestration-job', state: 'running', result_url: '/api/agent/jobs/doctor-orchestration-job/result', retry_after_seconds: 1 } }) ); } else { response.writeHead(400, { 'content-type': 'application/json' }); response.end( JSON.stringify({ error: { code: 'idempotency_key_required', message: 'missing key', retryable: false } }) ); } return; } if (request.url === '/api/agent/jobs/doctor-orchestration-job/result') { response.writeHead(200, { 'content-type': 'application/json' }); response.end( JSON.stringify({ request_id: 'doctor-orchestration-job', idempotency_key: 'agent-doctor-orchestration-generate', cached: false, images: [{ id: 'doctor-orchestration', filename: 'doctor-orchestration.png' }], execution: { transport: 'server_orchestrated', endpoint: '/api/agent/image-requests', route_mode: 'auto', image_backend: 'images-api', stream_mode: 'non_stream', streaming_strategy: 'off', channel_request_mode: 'images-non-stream', channel_request_mode_fallback_applied: false } }) ); return; } if (request.url === '/api/images') { (async () => { const requestText = await readRequestText(request); if (requestText.includes('contract check')) { response.writeHead(400, { 'content-type': 'application/json' }); response.end(JSON.stringify({ error: 'clientRequestId 长度不能超过 128 个字符。' })); return; } response.writeHead(503, { 'content-type': 'text/plain' }); response.end('503 Service temporarily unavailable'); })().catch((error) => { response.writeHead(500, { 'content-type': 'application/json' }); response.end(JSON.stringify({ error: error.message })); }); return; } if (request.url === '/api/agent/jobs/images/generate') { response.writeHead(400, { 'content-type': 'application/json' }); response.end( JSON.stringify({ error: { code: 'idempotency_key_required', message: 'missing key', retryable: false } }) ); return; } response.writeHead(404, { 'content-type': 'application/json' }); response.end(JSON.stringify({ error: 'missing' })); }, async (baseUrl) => { const result = await runNodeCommandAsync( ['scripts/agent-doctor.mjs', '--base-url', baseUrl, '--allow-billable'], { timeoutMs: 15_000 } ); assert.equal(result.ok, false); const body = parseJsonPayload(result.stdout, 'agent doctor'); assert.equal(body.ok, false); assert.equal(body.summary.billable_smoke, 'failed'); assert.equal(body.summary.page_sse_real_smoke, 'failed'); assert.equal(body.summary.orchestration_generate_smoke, 'passed'); assert.equal(body.summary.agent_generate_smoke, 'passed'); assert.equal(body.summary.responses_page_sse_generate_smoke, 'failed'); assert.equal(body.summary.responses_agent_generate_smoke, 'passed'); assert.equal(body.summary.real_smoke_checks.orchestration_generate_1k, 'passed'); assert.equal(body.summary.real_smoke_checks.agent_generate_1k, 'passed'); assert.equal(body.summary.real_smoke_checks.responses_page_sse_generate_1k, 'failed'); assert.equal(body.summary.real_smoke_checks.responses_agent_generate_1k, 'passed'); assert.equal(body.summary.request_modes.smoke['images-non-stream'].state, 'passed'); assert.equal(body.summary.request_modes.smoke['responses-non-stream'].state, 'passed'); assert.equal(body.summary.request_modes.smoke['responses-sse'].state, 'failed'); assert.deepEqual(body.summary.request_modes.effective, [ 'images-non-stream', 'responses-non-stream', 'responses-sse' ]); assert.equal( body.summary.request_modes.suggested_effective_value, 'images-non-stream,responses-non-stream' ); assert.deepEqual( body.summary.request_modes.gaps.find((gap) => gap.code === 'request_mode_smoke_failed'), { code: 'request_mode_smoke_failed', severity: 'critical', request_modes: ['responses-sse'], message: '真实 smoke 显示部分 request mode 不可用;不要写入渠道白名单。' } ); assert.equal(body.summary.real_smoke_checks.page_sse_edit_2k, 'skipped'); const pageSseOutput = body.layers .find((layer) => layer.name === 'billable_smoke') .checks.find((check) => check.name === 'responses_page_sse_generate_1k').output; const pageSseFailure = parseJsonPayload(pageSseOutput, 'responses page SSE doctor smoke'); assert.equal(pageSseFailure.summary.selected_channel_id, null); assert.equal(pageSseFailure.summary.upstream_host, null); assert.match(pageSseOutput, /503 Service temporarily unavailable/); } ); }); it('prints localized agent:doctor help and option errors', async () => { const help = await runNodeCommandAsync(['scripts/agent-doctor.mjs', '--help'], { env: process.env, timeoutMs: 5_000 }); assert.equal(help.ok, true); assert.match(help.stdout, /用法:/); assert.match(help.stdout, /真实 generate\/edit smoke/); assert.doesNotMatch(help.stdout, /Usage:/); const invalid = await runNodeCommandAsync(['scripts/agent-doctor.mjs', '--bad-option'], { env: process.env, timeoutMs: 5_000 }); assert.equal(invalid.ok, false); assert.match(invalid.stdout, /"ok": false/); assert.match(invalid.stdout, /未知参数:--bad-option/); }); it('preserves raw child output for command consumers', () => { const result = runCommand(process.execPath, ['-e', 'process.stdout.write(" M README.md\\n")']); assert.equal(result.ok, true); assert.equal(result.stdout, ' M README.md\n'); }); it('parses JSON output after command preamble lines', () => { assert.deepEqual(parseJsonPayload('hint\n{"ok":true}', 'example'), { ok: true }); assert.deepEqual(parseJsonPayload('hint\n{"ok":true}\ntrailing log', 'example'), { ok: true }); assert.deepEqual(parseJsonPayload('[WARN] not json\n{"ok":true}', 'example'), { ok: true }); assert.throws(() => parseJsonPayload('hint only', 'example'), /example did not return JSON output/); assert.throws(() => parseJsonPayload('hint\n{"ok":', 'example'), /example returned invalid JSON/); }); it('times out local HTTP probes instead of hanging', async () => { const server = createServer(() => {}); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); try { assert.equal(typeof address, 'object'); await assert.rejects( fetchJsonWithTimeout(`http://127.0.0.1:${address.port}/api/agent/capabilities`, { timeoutMs: 20 }), /timed out after 20ms/ ); } finally { await new Promise((resolve) => server.close(resolve)); } }); it('preserves invalid URL fetch errors while building timeout diagnostics', async () => { await assert.rejects( () => fetchJsonWithTimeout('not a url', { timeoutMs: 20 }), /Failed to parse URL|Invalid URL/i ); }); it('includes a response snippet when HTTP probes return non-JSON bodies', async () => { const server = createServer((request, response) => { response.writeHead(200, { 'content-type': 'text/html' }); response.end('login page'); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); try { assert.equal(typeof address, 'object'); await assert.rejects( fetchJsonWithTimeout(`http://127.0.0.1:${address.port}/api/agent/capabilities`, { timeoutMs: 1000 }), /did not return JSON: login page<\/html>/ ); } finally { await new Promise((resolve) => server.close(resolve)); } }); it('redacts response snippets from production HTTP probe errors', async () => { const originalNodeEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'production'; const server = createServer((request, response) => { response.writeHead(502, { 'content-type': 'text/plain' }); response.end('secret upstream body'); }); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); try { assert.equal(typeof address, 'object'); await assert.rejects( fetchJsonWithTimeout(`http://127.0.0.1:${address.port}/api/agent/capabilities`, { timeoutMs: 1000 }), (error) => { assert.match(error.message, /failed with HTTP 502/); assert.doesNotMatch(error.message, /secret upstream body/); return true; } ); } finally { if (originalNodeEnv === undefined) { delete process.env.NODE_ENV; } else { process.env.NODE_ENV = originalNodeEnv; } await new Promise((resolve) => server.close(resolve)); } }); it('supports command timeouts for long-running child processes', () => { const result = runCommand(process.execPath, ['-e', 'setTimeout(() => {}, 1000)'], { timeoutMs: 20 }); assert.equal(result.ok, false); assert.equal(result.status, null); assert.match(result.error, /timed out|ETIMEDOUT|SIGTERM/i); assert.match(pickFailureOutput(result), /error:|signal:/); }); it('includes spawn errors in failure summaries without stdout or stderr', () => { const summary = pickFailureOutput({ stdout: '', stderr: '', error: 'spawn missing ENOENT', signal: null }); assert.equal(summary, 'error: spawn missing ENOENT'); }); it('includes non-zero exit status in failure summaries', () => { const summary = pickFailureOutput({ stdout: '', stderr: 'bad option', status: 129, signal: null }); assert.equal(summary, 'bad option\nstatus: 129'); }); it('keeps local status usable when remote status JSON is malformed', () => { assert.deepEqual(readRemoteStatusFromResult({ ok: true, stdout: 'not json' }), { ok: false, error: 'hf spaces info did not return JSON output.' }); }); }); async function withServer(handler, run) { const server = createServer(handler); await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve)); const address = server.address(); try { assert.equal(typeof address, 'object'); assert.ok(address); await run(`http://127.0.0.1:${address.port}`); } finally { await new Promise((resolve) => server.close(resolve)); } } function runNodeCommandAsync(args, options = {}) { return new Promise((resolve) => { const child = spawn(process.execPath, args, { cwd: options.cwd || process.cwd(), env: { GPT_IMAGE_AGENT_LOAD_ENV_FILE: '0', ...options.env }, stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; const startedAt = Date.now(); const timeout = options.timeoutMs ? setTimeout(() => { child.kill('SIGTERM'); }, options.timeoutMs) : undefined; child.stdout.setEncoding('utf8'); child.stderr.setEncoding('utf8'); child.stdout.on('data', (chunk) => { stdout += chunk; }); child.stderr.on('data', (chunk) => { stderr += chunk; }); child.on('close', (status, signal) => { if (timeout) clearTimeout(timeout); resolve({ ok: status === 0, status, signal, stdout, stderr, elapsed_ms: Date.now() - startedAt }); }); }); } async function writeExecutable(filepath, content) { await writeFile(filepath, content); await chmod(filepath, 0o755); } function readRequestText(request) { return new Promise((resolve, reject) => { let body = ''; request.setEncoding('utf8'); request.on('data', (chunk) => { body += chunk; }); request.on('end', () => resolve(body)); request.on('error', reject); }); }