visual-journal / scripts /command-center.test.mjs
Visual Journal deploy
Deploy f77fc10 to Docker Space
805101e
Raw
History Blame Contribute Delete
98.1 kB
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('<html>login page</html>');
});
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: <html>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);
});
}