visual-journal / scripts /agent-skill-scripts.test.mjs
gpt-image-playground deploy
Deploy b761290 to Docker Space
f250aec
Raw
History Blame Contribute Delete
437 kB
import { AGENT_ENDPOINTS } from '../skills/gpt-image-playground-agent/scripts/lib/agent-api-paths.mjs';
import { enrichFailureWithAgentDiagnostics } from '../skills/gpt-image-playground-agent/scripts/lib/agent-diagnostics-summary.mjs';
import { AGENT_ENDPOINTS as SERVER_AGENT_ENDPOINTS } from '../src/lib/agent-api-paths.mjs';
import {
parseRetryAfterValue,
readCapabilitiesImageTransportTimeoutMs,
resolveSameOriginUrl
} from '../skills/gpt-image-playground-agent/scripts/lib/script-utils.mjs';
import { FIXTURE_IMAGE_BASE64 } from './local-image-upstream-fixture.mjs';
import assert from 'node:assert/strict';
import { spawn, spawnSync } from 'node:child_process';
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
import { createServer } from 'node:http';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, it } from 'node:test';
import { fileURLToPath } from 'node:url';
const repoRoot = fileURLToPath(new URL('..', import.meta.url));
const skillRoot = join(repoRoot, 'skills/gpt-image-playground-agent');
const skillScriptsRoot = join(repoRoot, 'skills/gpt-image-playground-agent/scripts');
const localUpstreamProbeTimeoutMs = '5000';
function agentGenerateCapabilities(extra = {}) {
return {
orchestration: {
supported: true,
transport_selection: 'server_owned',
endpoint: '/api/agent/image-requests'
},
agent_jobs: { supported: true, mode: 'job_polling' },
limits: {
generate_images: { min: 1, max: 4 },
partial_images: { min: 0, max: 4 },
partial_images_by_backend: {
'images-api': { min: 0, max: 4 },
'responses-image-generation': { min: 1, max: 3 }
},
upload_images: { max: 8 }
},
defaults: { partial_images: 2 },
...extra
};
}
describe('Agent skill script argument validation', () => {
it('rejects invalid generate numeric options before dry-run output', () => {
const result = runSkillScript('generate-image.mjs', ['--n', 'abc', 'prompt']);
assert.equal(result.status, 2);
assert.match(result.stderr, /--n 必须是正整数/);
assert.equal(result.stdout.trim(), '');
});
it('rejects invalid edit timeout before reading the image path', () => {
const result = runSkillScript('edit-image.mjs', ['--timeout-ms', 'abc', '/tmp/missing.png', 'prompt']);
assert.equal(result.status, 2);
assert.match(result.stderr, /--timeout-ms 必须是正整数/);
assert.equal(result.stdout.trim(), '');
});
it('accepts --image as an edit image path alias', () => {
const result = runSkillScript('edit-image.mjs', ['--image', '/tmp/source.png', 'prompt']);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.request.image_path, '/tmp/source.png');
assert.equal(body.request.prompt, 'prompt');
});
it('rejects mixing --image with positional edit image path', () => {
const result = runSkillScript('edit-image.mjs', ['/tmp/source.png', '--image', '/tmp/other.png', 'prompt']);
assert.equal(result.status, 2);
assert.match(result.stderr, /--image 与位置参数 <image-path> 不能同时设置/);
assert.equal(result.stdout.trim(), '');
});
it('rejects repeated --image edit image aliases', () => {
const result = runSkillScript('edit-image.mjs', [
'--image',
'/tmp/source.png',
'--image',
'/tmp/other.png',
'prompt'
]);
assert.equal(result.status, 2);
assert.match(result.stderr, /--image 只能设置一次/);
assert.equal(result.stdout.trim(), '');
});
it('rejects invalid upstream probe timeout before network checks', () => {
const result = runSkillScript('probe-upstream-image.mjs', ['--timeout-ms', 'abc']);
assert.equal(result.status, 2);
assert.match(result.stderr, /--timeout-ms 必须是正整数/);
assert.equal(result.stdout.trim(), '');
});
it('rejects malformed or non-positive image sizes before dry-run or network requests', () => {
const generateResult = runSkillScript('generate-image.mjs', ['--size', '0x2048', 'prompt']);
assert.equal(generateResult.status, 2);
assert.match(generateResult.stderr, /--size 的宽度和高度必须是正数/);
assert.equal(generateResult.stdout.trim(), '');
const editResult = runSkillScript('edit-image.mjs', ['--size', '2048x0', '/tmp/source.png', 'prompt']);
assert.equal(editResult.status, 2);
assert.match(editResult.stderr, /--size 的宽度和高度必须是正数/);
assert.equal(editResult.stdout.trim(), '');
const probeResult = runSkillScript('probe-upstream-image.mjs', ['--size', 'invalid-size']);
assert.equal(probeResult.status, 2);
assert.match(probeResult.stderr, /--size 必须是 auto 或 WIDTHxHEIGHT/);
assert.equal(probeResult.stdout.trim(), '');
const legacyForceResult = runSkillScript('generate-image.mjs', [
'--model',
'gpt-image-1',
'--force-request',
'--size',
'512x512',
'prompt'
]);
assert.equal(legacyForceResult.status, 2);
assert.match(legacyForceResult.stderr, /非 gpt-image-2 只支持 auto、1024x1024、1536x1024、1024x1536/);
assert.equal(legacyForceResult.stdout.trim(), '');
});
it('caps Agent diagnostics enrichment timeout independently of image request timeouts', async () => {
const startedAt = Date.now();
const result = await enrichFailureWithAgentDiagnostics({
baseUrl: 'http://example.test/playground',
authHeaders: () => ({}),
idempotencyKey: 'slow-diagnostics-key',
failureOutput: { ok: false, error: { code: 'network_error' } },
summary: {
ok: false,
retryable: true,
next_action: 'retry_after_wait'
},
timeoutMs: 420000,
diagnosticsTimeoutMs: 25,
fetchFn: (_url, init) =>
new Promise((_resolve, reject) => {
init.signal.addEventListener('abort', () => {
const error = new Error('aborted');
error.name = 'AbortError';
reject(error);
});
})
});
assert.equal(result.summary.agent_diagnostics_checked, true);
assert.equal(result.summary.agent_diagnostics_found, false);
assert.equal(result.summary.agent_diagnostics_unavailable_reason, 'diagnostics_timeout');
assert.equal(result.failureOutput.agent_failure_diagnostics.unavailable_reason, 'diagnostics_timeout');
assert.equal(result.summary.retryable, true);
assert.equal(result.summary.next_action, 'retry_after_wait');
assert.ok(Date.now() - startedAt < 1000);
});
it('reports non-json Agent diagnostics responses distinctly', async () => {
const result = await enrichFailureWithAgentDiagnostics({
baseUrl: 'http://example.test/playground',
authHeaders: () => ({}),
idempotencyKey: 'html-diagnostics-key',
failureOutput: { ok: false, error: { code: 'network_error' } },
summary: { ok: false },
timeoutMs: 420000,
fetchFn: async () => ({
ok: false,
status: 502,
headers: new Headers({ 'content-type': 'text/html' }),
text: async () => '<html>bad gateway</html>'
})
});
assert.equal(result.summary.agent_diagnostics_checked, true);
assert.equal(result.summary.agent_diagnostics_found, false);
assert.equal(result.summary.agent_diagnostics_unavailable_reason, 'non_json_response');
assert.equal(result.summary.agent_diagnostics_http_status, 502);
assert.equal(result.failureOutput.agent_failure_diagnostics.unavailable_reason, 'non_json_response');
assert.equal(result.failureOutput.agent_failure_diagnostics.http_status, 502);
});
it('treats found Agent diagnostics without payload as invalid', async () => {
const result = await enrichFailureWithAgentDiagnostics({
baseUrl: 'http://example.test/playground',
authHeaders: () => ({}),
idempotencyKey: 'missing-diagnostics-key',
failureOutput: { ok: false, error: { code: 'network_error' } },
summary: { ok: false },
timeoutMs: 420000,
fetchFn: async () => ({
ok: true,
status: 200,
headers: new Headers({ 'content-type': 'application/json' }),
text: async () => JSON.stringify({ found: true })
})
});
assert.equal(result.summary.agent_diagnostics_checked, true);
assert.equal(result.summary.agent_diagnostics_found, false);
assert.equal(result.summary.agent_diagnostics_unavailable_reason, 'invalid_response');
assert.equal(result.summary.agent_diagnostics_http_status, 200);
assert.equal(result.failureOutput.agent_failure_diagnostics.found, false);
assert.equal(result.failureOutput.agent_failure_diagnostics.unavailable_reason, 'invalid_response');
assert.equal(result.failureOutput.agent_failure_diagnostics.http_status, 200);
assert.equal(result.failureOutput.agent_failure_diagnostics.request_id, undefined);
});
it('converts local images to webp by default', () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-convert-'));
try {
const inputPath = join(tempRoot, 'source.png');
writeFileSync(inputPath, validPngBuffer());
const result = runSkillScript('convert-image-format.mjs', [inputPath]);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.ok, true);
assert.equal(body.output.format, 'webp');
assert.equal(body.output.quality, 100);
assert.match(body.output.path, /source\.webp$/);
const output = readFileSync(body.output.path);
assert.equal(output.toString('ascii', 0, 4), 'RIFF');
assert.equal(output.toString('ascii', 8, 12), 'WEBP');
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('rejects invalid conversion quality before writing output', () => {
const result = runSkillScript('convert-image-format.mjs', ['--quality', '101', '/tmp/source.png']);
assert.equal(result.status, 2);
assert.match(result.stderr, /--quality 必须是 1 到 100/);
assert.equal(result.stdout.trim(), '');
});
it('rejects invalid retry attempt env values', () => {
const result = runSkillScript('generate-image.mjs', ['prompt'], {
GPT_IMAGE_AGENT_MAX_ATTEMPTS: 'abc'
});
assert.equal(result.status, 2);
assert.match(result.stderr, /GPT_IMAGE_AGENT_MAX_ATTEMPTS 必须是正整数/);
assert.equal(result.stdout.trim(), '');
});
it('loads private agent env files for CLI scripts without overriding shell env', () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'agent-env-load-'));
try {
mkdirSync(join(tempRoot, '.git'));
writeFileSync(
join(tempRoot, '.env.agent.local'),
[
'GPT_IMAGE_PLAYGROUND_URL=https://file-space.example.test',
'GPT_IMAGE_AGENT_MAX_ATTEMPTS=2',
'GPT_IMAGE_AGENT_TOKEN=file-token',
'GPT_IMAGE_APP_PASSWORD_HASH=file-hash',
'GPT_IMAGE_UPSTREAM_BASE_URL=https://upstream.example.test/v1',
'GPT_IMAGE_UPSTREAM_API_KEY=upstream-secret'
].join('\n')
);
const loaded = runSkillScript(
'generate-image.mjs',
['prompt'],
{},
{ cwd: tempRoot, loadPrivateAgentEnv: true }
);
assert.equal(loaded.status, 0);
const loadedBody = JSON.parse(loaded.stdout);
assert.equal(loadedBody.verification_scope.service_base_url, 'https://file-space.example.test');
assert.equal(loadedBody.verification_scope.service_base_url_source, 'GPT_IMAGE_PLAYGROUND_URL');
assert.equal(loadedBody.verification_scope.interactive_confirmation_required, true);
assert.doesNotMatch(loaded.stdout, /file-token|file-hash|upstream-secret/);
assert.equal(loaded.stderr.trim(), '');
const fromNestedCwd = runSkillScript(
'generate-image.mjs',
['prompt'],
{},
{ cwd: join(tempRoot, 'nested', 'scripts'), loadPrivateAgentEnv: true, createCwd: true }
);
assert.equal(fromNestedCwd.status, 0);
const nestedBody = JSON.parse(fromNestedCwd.stdout);
assert.equal(nestedBody.verification_scope.service_base_url, 'https://file-space.example.test');
const parentRoot = mkdtempSync(join(tmpdir(), 'agent-env-parent-'));
try {
writeFileSync(
join(parentRoot, '.env.agent.local'),
'GPT_IMAGE_PLAYGROUND_URL=https://parent-space.example.test'
);
const childRoot = join(parentRoot, 'child-repo');
mkdirSync(join(childRoot, '.git'), { recursive: true });
const bounded = runSkillScript(
'generate-image.mjs',
['prompt'],
{},
{ cwd: join(childRoot, 'nested'), loadPrivateAgentEnv: true, createCwd: true }
);
assert.equal(bounded.status, 0);
const boundedBody = JSON.parse(bounded.stdout);
assert.equal(boundedBody.verification_scope.service_base_url, 'http://localhost:4783');
const projectCopyRoot = join(parentRoot, 'project-copy');
mkdirSync(join(projectCopyRoot, 'skills/gpt-image-playground-agent'), { recursive: true });
mkdirSync(join(projectCopyRoot, 'nested'), { recursive: true });
writeFileSync(join(projectCopyRoot, 'package.json'), JSON.stringify({ name: 'gpt-image-playground' }));
writeFileSync(join(projectCopyRoot, 'skills/gpt-image-playground-agent/SKILL.md'), '# skill\n');
const projectBounded = runSkillScript(
'generate-image.mjs',
['prompt'],
{},
{ cwd: join(projectCopyRoot, 'nested'), loadPrivateAgentEnv: true }
);
assert.equal(projectBounded.status, 0);
const projectBoundedBody = JSON.parse(projectBounded.stdout);
assert.equal(projectBoundedBody.verification_scope.service_base_url, 'http://localhost:4783');
writeFileSync(
join(projectCopyRoot, '.env.agent.local'),
'GPT_IMAGE_PLAYGROUND_URL=https://project-copy.example.test'
);
const projectLoaded = runSkillScript(
'generate-image.mjs',
['prompt'],
{},
{ cwd: join(projectCopyRoot, 'nested'), loadPrivateAgentEnv: true }
);
assert.equal(projectLoaded.status, 0);
const projectLoadedBody = JSON.parse(projectLoaded.stdout);
assert.equal(
projectLoadedBody.verification_scope.service_base_url,
'https://project-copy.example.test'
);
const projectSkillScriptsLoaded = runSkillScript(
'generate-image.mjs',
['prompt'],
{},
{
cwd: join(projectCopyRoot, 'skills/gpt-image-playground-agent/scripts'),
loadPrivateAgentEnv: true,
createCwd: true
}
);
assert.equal(projectSkillScriptsLoaded.status, 0);
const projectSkillScriptsLoadedBody = JSON.parse(projectSkillScriptsLoaded.stdout);
assert.equal(
projectSkillScriptsLoadedBody.verification_scope.service_base_url,
'https://project-copy.example.test'
);
} finally {
rmSync(parentRoot, { recursive: true, force: true });
}
const shellWins = runSkillScript(
'generate-image.mjs',
['prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: 'https://shell-space.example.test' },
{ cwd: tempRoot, loadPrivateAgentEnv: true }
);
assert.equal(shellWins.status, 0);
const shellBody = JSON.parse(shellWins.stdout);
assert.equal(shellBody.verification_scope.service_base_url, 'https://shell-space.example.test');
const disabled = runSkillScript(
'generate-image.mjs',
['prompt'],
{ GPT_IMAGE_AGENT_LOAD_ENV_FILE: '0' },
{ cwd: tempRoot }
);
assert.equal(disabled.status, 0);
const disabledBody = JSON.parse(disabled.stdout);
assert.equal(disabledBody.verification_scope.service_base_url, 'http://localhost:4783');
const probe = runSkillScript(
'probe-upstream-image.mjs',
['--timeout-ms', '1'],
{},
{ cwd: tempRoot, loadPrivateAgentEnv: true }
);
assert.notEqual(probe.status, 2);
const probeBody = JSON.parse(probe.stdout);
assert.equal(probeBody.base_url, 'https://upstream.example.test/v1');
assert.equal(probeBody.api_key_configured, true);
assert.doesNotMatch(probe.stdout, /upstream-secret/);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('inherits longer image transport timeout from capabilities by default', () => {
assert.equal(
readCapabilitiesImageTransportTimeoutMs({ image_transport: { upstream_timeout_ms: 900_000 } }, 420_000),
900_000
);
assert.equal(
readCapabilitiesImageTransportTimeoutMs({ image_transport: { upstream_timeout_ms: 300_000 } }, 420_000),
420_000
);
});
it('rejects service base URLs with embedded credentials before dry-run output', () => {
const result = runSkillScript('generate-image.mjs', ['prompt'], {
GPT_IMAGE_PLAYGROUND_URL: 'https://user:secret@example.test'
});
assert.equal(result.status, 2);
assert.match(result.stderr, /base URL/);
assert.match(result.stderr, /不能包含凭据/);
assert.doesNotMatch(result.stderr, /secret/);
assert.equal(result.stdout.trim(), '');
});
it('lets explicit base-url override the environment service URL in dry-run output', () => {
const generateResult = runSkillScript(
'generate-image.mjs',
['--base-url', 'https://space.example.test/', '--size', '1024x1024', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: 'http://localhost:4783' }
);
assert.equal(generateResult.status, 0);
const generateBody = JSON.parse(generateResult.stdout);
assert.equal(generateBody.verification_scope.service_base_url, 'https://space.example.test');
assert.equal(generateBody.verification_scope.service_base_url_source, 'user_provided');
assert.equal(generateBody.verification_scope.interactive_confirmation_required, false);
const editResult = runSkillScript(
'edit-image.mjs',
['--base-url', 'https://space.example.test/', '--image', '/tmp/source.png', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: 'http://localhost:4783' }
);
assert.equal(editResult.status, 0);
const editBody = JSON.parse(editResult.stdout);
assert.equal(editBody.verification_scope.service_base_url, 'https://space.example.test');
assert.equal(editBody.verification_scope.service_base_url_source, 'user_provided');
const tempRoot = mkdtempSync(join(tmpdir(), 'batch-base-url-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(inputPath, JSON.stringify({ id: 'first', prompt: 'prompt' }));
const batchResult = runSkillScript(
'batch-images.mjs',
['--base-url', 'https://space.example.test/', '--input', inputPath],
{ GPT_IMAGE_PLAYGROUND_URL: 'http://localhost:4783' }
);
assert.equal(batchResult.status, 0);
const batchBody = JSON.parse(batchResult.stdout);
assert.equal(batchBody.verification_scope.service_base_url, 'https://space.example.test');
assert.equal(batchBody.verification_scope.service_base_url_source, 'user_provided');
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('rejects upstream probe base URLs with embedded credentials before network checks', () => {
const result = runSkillScript('probe-upstream-image.mjs', [
'--base-url',
'https://user:secret@example.test/v1'
]);
assert.equal(result.status, 2);
assert.match(result.stderr, /base URL/);
assert.match(result.stderr, /不能包含凭据/);
assert.doesNotMatch(result.stderr, /secret/);
assert.equal(result.stdout.trim(), '');
});
it('emits a non-billable upstream probe summary with stable request headers', async () => {
let userAgent = '';
await withServer(
(request, response) => {
userAgent = String(request.headers['user-agent'] || '');
if (request.url === '/v1/models') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ data: [{ id: 'gpt-image-2' }] }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync('probe-upstream-image.mjs', [
'--base-url',
`${baseUrl}/v1`,
'--timeout-ms',
localUpstreamProbeTimeoutMs
]);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.ok, true);
assert.equal(body.billable, false);
assert.equal(body.models.model_count, 1);
assert.equal(body.models.first_model_id, 'gpt-image-2');
assert.equal(body.models.image_count, undefined);
assert.equal(body.models.first_b64_length, undefined);
assert.equal(body.summary.ok, true);
assert.equal(body.summary.billable, false);
assert.equal(body.summary.transport, 'upstream_probe');
assert.equal(body.summary.endpoint.endsWith('/v1/models'), true);
assert.equal(body.summary.request_headers.user_agent_effective, 'visual-journal/probe');
assert.equal(body.summary.request_headers.has_extra_headers, false);
assert.deepEqual(body.summary.request_headers.allowed_header_names, ['authorization', 'user-agent']);
assert.deepEqual(body.summary.request_headers.configured_header_names, []);
assert.equal(userAgent, 'visual-journal/probe');
}
);
});
it('uses an explicit connection IP without changing the upstream hostname', async () => {
let hostHeader = '';
await withServer(
(request, response) => {
hostHeader = String(request.headers.host || '');
if (request.url === '/v1/models') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ data: [{ id: 'gpt-image-2' }] }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const localUrl = new URL(baseUrl);
const result = await runSkillScriptAsync('probe-upstream-image.mjs', [
'--base-url',
`http://upstream.invalid:${localUrl.port}/v1`,
'--connect-ip',
'127.0.0.1',
'--timeout-ms',
localUpstreamProbeTimeoutMs
]);
assert.equal(result.status, 0, `${result.stderr}\n${result.stdout}`);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.connect_ip, '127.0.0.1');
assert.equal(body.summary.connect_ip, '127.0.0.1');
assert.equal(body.dns.reason, 'connect_ip_override');
assert.equal(body.dns.address, '127.0.0.1');
assert.equal(body.tls.reason, 'non_https_base_url');
assert.equal(body.models.first_model_id, 'gpt-image-2');
assert.equal(hostHeader, `upstream.invalid:${localUrl.port}`);
}
);
});
it('reports configured upstream probe authorization without exposing the key', async () => {
await withServer(
(request, response) => {
assert.equal(request.headers.authorization, 'Bearer probe-secret');
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ data: [{ id: 'gpt-image-2' }] }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'probe-upstream-image.mjs',
['--base-url', `${baseUrl}/v1`, '--timeout-ms', localUpstreamProbeTimeoutMs],
{ GPT_IMAGE_UPSTREAM_API_KEY: 'probe-secret', OPENAI_UPSTREAM_USER_AGENT: 'custom-probe' }
);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.summary.request_headers.user_agent_effective, 'custom-probe');
assert.equal(body.summary.request_headers.has_extra_headers, true);
assert.deepEqual(body.summary.request_headers.configured_header_names, ['authorization', 'user-agent']);
assert.equal(JSON.stringify(body).includes('probe-secret'), false);
}
);
});
it('probes explicit request modes and classifies Responses 403 as unavailable', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push(`${request.method} ${request.url}`);
if (request.url === '/v1/models') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ data: [{ id: 'gpt-image-2' }] }));
return;
}
if (request.url === '/v1/images/generations') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ data: [{ b64_json: 'YmFzZTY0LWltYWdl' }] }));
return;
}
if (request.url === '/v1/responses') {
response.writeHead(403, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
error: {
code: 'permission_denied',
message: 'Image generation is not enabled for this group',
type: 'invalid_request_error'
}
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync('probe-upstream-image.mjs', [
'--base-url',
`${baseUrl}/v1`,
'--allow-billable',
'--request-mode',
'images-non-stream,responses-non-stream',
'--responses-model',
'gpt-4.1-mini',
'--timeout-ms',
'1000'
]);
assert.equal(result.status, 1);
const body = JSON.parse(result.stdout);
assert.equal(body.ok, false);
assert.equal(body.billable, true);
assert.deepEqual(body.request_modes.requested, ['images-non-stream', 'responses-non-stream']);
assert.deepEqual(body.request_modes.passed, ['images-non-stream']);
assert.deepEqual(body.request_modes.failed, ['responses-non-stream']);
assert.deepEqual(body.request_modes.skipped, []);
assert.deepEqual(body.request_modes.not_selected, ['images-sse', 'responses-sse']);
assert.equal(body.request_modes.suggested_channel_config, 'images-non-stream');
assert.equal(body.request_modes.suggested_env_key, 'OPENAI_CHANNEL_N_REQUEST_MODES');
assert.equal(body.request_modes.modes['images-non-stream'].ok, true);
assert.equal(body.request_modes.modes['responses-non-stream'].ok, false);
assert.equal(body.request_modes.modes['responses-non-stream'].category, 'responses_disabled');
assert.match(body.request_modes.modes['responses-non-stream'].error, /Image generation is not enabled/);
assert.equal(body.request_modes.modes['responses-non-stream'].responses_model, 'gpt-4.1-mini');
assert.deepEqual(requests, ['GET /v1/models', 'POST /v1/images/generations', 'POST /v1/responses']);
}
);
});
it('does not suggest remote URL-only Responses results as usable request modes', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push(`${request.method} ${request.url}`);
if (request.url === '/v1/models') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ data: [{ id: 'gpt-image-2' }] }));
return;
}
if (request.url === '/v1/responses') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
output: [
{
type: 'image_generation_call',
status: 'completed',
url: 'https://cdn.example.test/final.png'
}
]
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync('probe-upstream-image.mjs', [
'--base-url',
`${baseUrl}/v1`,
'--allow-billable',
'--request-mode',
'responses-non-stream',
'--responses-model',
'gpt-4.1-mini',
'--timeout-ms',
'1000'
]);
assert.equal(result.status, 1);
const body = JSON.parse(result.stdout);
const mode = body.request_modes.modes['responses-non-stream'];
assert.deepEqual(body.request_modes.passed, []);
assert.deepEqual(body.request_modes.failed, ['responses-non-stream']);
assert.equal(body.request_modes.suggested_channel_config, '');
assert.equal(mode.ok, false);
assert.equal(mode.error, 'url_only_result');
assert.equal(mode.has_url_result, true);
assert.equal(mode.has_remote_url_result, true);
assert.equal(mode.has_consumable_image, false);
assert.deepEqual(requests, ['GET /v1/models', 'POST /v1/responses']);
}
);
});
it('treats data URL image results as consumable in upstream probe output', async () => {
await withServer(
(request, response) => {
if (request.url === '/v1/models') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ data: [{ id: 'gpt-image-2' }] }));
return;
}
if (request.url === '/v1/responses') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
output: [
{
type: 'image_generation_call',
status: 'completed',
url: 'data:image/png;base64,YmFzZTY0LWltYWdl'
}
]
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync('probe-upstream-image.mjs', [
'--base-url',
`${baseUrl}/v1`,
'--allow-billable',
'--request-mode',
'responses-non-stream',
'--responses-model',
'gpt-4.1-mini',
'--timeout-ms',
'1000'
]);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
const mode = body.request_modes.modes['responses-non-stream'];
assert.deepEqual(body.request_modes.passed, ['responses-non-stream']);
assert.equal(body.request_modes.suggested_channel_config, 'responses-non-stream');
assert.equal(mode.ok, true);
assert.equal(mode.has_inline_base64, true);
assert.equal(mode.has_consumable_image, true);
assert.equal(mode.has_remote_url_result, false);
}
);
});
it('does not mark pending Responses image calls as a usable request mode', async () => {
await withServer(
(request, response) => {
if (request.url === '/v1/models') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ data: [{ id: 'gpt-image-2' }] }));
return;
}
if (request.url === '/v1/responses') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
output: [
{
type: 'image_generation_call',
status: 'pending',
result: FIXTURE_IMAGE_BASE64
}
]
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync('probe-upstream-image.mjs', [
'--base-url',
`${baseUrl}/v1`,
'--allow-billable',
'--request-mode',
'responses-non-stream',
'--responses-model',
'gpt-4.1-mini',
'--timeout-ms',
'1000'
]);
assert.equal(result.status, 1);
const body = JSON.parse(result.stdout);
const mode = body.request_modes.modes['responses-non-stream'];
assert.deepEqual(body.request_modes.passed, []);
assert.deepEqual(body.request_modes.failed, ['responses-non-stream']);
assert.equal(mode.ok, false);
assert.equal(mode.error, 'missing_image_call_result');
assert.equal(mode.has_consumable_image, false);
}
);
});
it('saves the first final upstream SSE image only when explicitly requested', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'probe-upstream-save-'));
const outputPath = join(tempRoot, 'probe.png');
const laterImageBase64 = fakePngBase64(2, 1);
try {
await withServer(
(request, response) => {
if (request.url === '/v1/models') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ data: [{ id: 'gpt-image-2' }] }));
return;
}
if (request.url === '/v1/images/generations') {
response.writeHead(200, {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
connection: 'keep-alive'
});
response.write(
'event: image_generation.partial_image\n' +
'data: {"type":"image_generation.partial_image","b64_json":"YmFzZTY0LWltYWdl"}\n\n'
);
response.write(
'event: image_generation.completed\n' +
`data: {"type":"image_generation.completed","b64_json":"${FIXTURE_IMAGE_BASE64}"}\n\n`
);
response.write(
'event: image_generation.completed\n' +
`data: {"type":"image_generation.completed","b64_json":"${laterImageBase64}"}\n\n`
);
response.end('data: [DONE]\n\n');
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync('probe-upstream-image.mjs', [
'--base-url',
baseUrl + '/v1',
'--allow-billable',
'--request-mode',
'images-sse',
'--save-first-image',
outputPath,
'--timeout-ms',
'1000'
]);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
const mode = body.request_modes.modes['images-sse'];
assert.equal(mode.saved_image.ok, true);
assert.equal(mode.saved_image.path, outputPath);
assert.equal(mode.saved_image.byte_length, Buffer.from(FIXTURE_IMAGE_BASE64, 'base64').byteLength);
assert.deepEqual(mode.saved_image.dimensions, { width: 1, height: 1 });
assert.equal(mode.saved_image.source, 'b64_json');
assert.equal(readFileSync(outputPath).toString('base64'), FIXTURE_IMAGE_BASE64);
assert.equal(result.stdout.includes(FIXTURE_IMAGE_BASE64), false);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('does not save partial-only upstream SSE previews as final images', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'probe-upstream-partial-only-'));
const outputPath = join(tempRoot, 'probe.png');
try {
await withServer(
(request, response) => {
if (request.url === '/v1/models') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ data: [{ id: 'gpt-image-2' }] }));
return;
}
if (request.url === '/v1/images/generations') {
response.writeHead(200, {
'content-type': 'text/event-stream',
'cache-control': 'no-cache',
connection: 'keep-alive'
});
response.write(
'event: image_generation.partial_image\n' +
`data: {"type":"image_generation.partial_image","b64_json":"${FIXTURE_IMAGE_BASE64}"}\n\n`
);
response.end('data: [DONE]\n\n');
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync('probe-upstream-image.mjs', [
'--base-url',
`${baseUrl}/v1`,
'--allow-billable',
'--request-mode',
'images-sse',
'--save-first-image',
outputPath,
'--timeout-ms',
'1000'
]);
assert.equal(result.status, 1);
const body = JSON.parse(result.stdout);
const mode = body.request_modes.modes['images-sse'];
assert.equal(mode.ok, false);
assert.equal(mode.error, 'missing_final_image');
assert.equal(mode.has_partial_image, true);
assert.equal(mode.saved_image, undefined);
assert.equal(existsSync(outputPath), false);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('does not read prompt files during default generate dry-run', () => {
const result = runSkillScript('generate-image.mjs', ['--prompt-file', '/tmp/missing-agent-prompt.txt']);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.dry_run, true);
assert.equal(body.billable, false);
assert.equal(body.verification_scope.mode, 'local_planning_only');
assert.equal(body.verification_scope.remote_capabilities_verified, false);
assert.equal(body.verification_scope.runtime_capacity_verified, false);
assert.equal(body.verification_scope.auth_verified, false);
assert.equal(body.request.output_format, 'webp');
assert.equal(body.request.output_compression, 100);
assert.equal(result.stderr.trim(), '');
});
it('includes explicit upstream streaming options in generate dry-run requests', () => {
const result = runSkillScript('generate-image.mjs', [
'--image-backend',
'responses',
'--stream-mode',
'stream',
'--streaming-strategy',
'responses-sse',
'--partial-images',
'3',
'prompt'
]);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.request.image_backend, 'responses');
assert.equal(body.request.stream_mode, 'stream');
assert.equal(body.request.streaming_strategy, 'responses-sse');
assert.equal(body.request.partial_images, 3);
assert.equal(result.stderr.trim(), '');
});
it('keeps GPT2Image-compatible generate options on server orchestration dry-runs', () => {
const result = runSkillScript('generate-image.mjs', [
'--image-backend',
'responses',
'--responses-model',
'gpt-5.4-mini',
'--thinking',
'medium',
'--prompt-optimization',
'false',
'--force-web',
'prompt'
]);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.endpoint, 'http://localhost:4783/api/agent/image-requests');
assert.equal(body.routing_guidance.transport, 'server_orchestrated');
assert.equal(body.request.image_backend, 'responses');
assert.equal(body.request.responsesModel, 'gpt-5.4-mini');
assert.equal(body.request.thinking, 'medium');
assert.equal(body.request.promptOptimization, false);
assert.equal(body.request.force_web, true);
assert.equal(result.stderr.trim(), '');
});
it('includes explicit upstream streaming options in edit dry-run requests', () => {
const result = runSkillScript('edit-image.mjs', [
'--stream-mode',
'auto',
'--streaming-strategy',
'force-sse',
'--partial-images',
'2',
'/tmp/source.png',
'prompt'
]);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.request.stream_mode, 'auto');
assert.equal(body.request.streaming_strategy, 'force-sse');
assert.equal(body.request.partial_images, 2);
assert.equal(body.verification_scope.mode, 'local_planning_only');
assert.equal(body.verification_scope.remote_capabilities_verified, false);
assert.equal(body.verification_scope.runtime_capacity_verified, false);
assert.equal(body.verification_scope.auth_verified, false);
assert.equal(result.stderr.trim(), '');
});
it('prints server orchestration guidance for high-resolution generate dry-runs', () => {
const result = runSkillScript('generate-image.mjs', ['--size', '3072x2048', '--quality', 'high', 'prompt']);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.endpoint, 'http://localhost:4783/api/agent/image-requests');
assert.equal(body.routing_guidance.recommended_endpoint, '/api/agent/image-requests');
assert.equal(body.routing_guidance.transport, 'server_orchestrated');
assert.equal(body.routing_guidance.result_mode, 'job_polling');
assert.match(body.routing_guidance.reason, /服务端负责选择内部执行路径/);
assert.equal(result.stderr.trim(), '');
});
it('rejects single generate dimension-check without a fixed requested size before remote execution', () => {
const result = runSkillScript('generate-image.mjs', ['--dimension-check', '--size', 'auto', 'prompt']);
assert.equal(result.status, 2);
assert.equal(result.stdout.trim(), '');
assert.match(result.stderr, /--dimension-check 需要 --size 为 WIDTHxHEIGHT/);
});
it('explains explicit page SSE dry-runs without blaming high-resolution routing', () => {
const result = runSkillScript('generate-image.mjs', ['--page-sse', '--size', '1024x1024', 'prompt']);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.endpoint, 'http://localhost:4783/api/images');
assert.equal(body.routing_guidance.transport, 'page_sse');
assert.match(body.routing_guidance.reason, /Explicit --page-sse/);
assert.doesNotMatch(body.routing_guidance.reason, /max_edge>2048/);
assert.equal(result.stderr.trim(), '');
});
it('prints server orchestration guidance for remote HTTPS small generate dry-runs', () => {
const result = runSkillScript('generate-image.mjs', ['--base-url', 'https://space.example.test', 'prompt']);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.endpoint, 'https://space.example.test/api/agent/image-requests');
assert.equal(body.routing_guidance.recommended_endpoint, '/api/agent/image-requests');
assert.equal(body.routing_guidance.transport, 'server_orchestrated');
assert.equal(body.routing_guidance.strength, 'recommended');
assert.match(body.routing_guidance.reason, /只提交生成意图/);
assert.equal(result.stderr.trim(), '');
});
it('can combine generate dry-run planning with non-billable remote checks', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify(
agentGenerateCapabilities({
supported: {
request_modes: ['images-non-stream', 'images-sse']
},
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
}
})
)
);
return;
}
if (request.url === '/api/runtime-capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
streamingBatch: { enabled: true, recommendedConcurrency: 2 },
channelRouting: { effectiveRequestModes: ['images-non-stream'] }
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync('generate-image.mjs', ['--check-remote', 'prompt'], {
GPT_IMAGE_PLAYGROUND_URL: baseUrl
});
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.verification_scope.mode, 'remote_contract_and_local_planning');
assert.equal(body.verification_scope.remote_capabilities_verified, true);
assert.equal(body.verification_scope.runtime_capacity_verified, true);
assert.equal(body.verification_scope.auth_verified, true);
assert.equal(body.verification_scope.billable_request_sent, false);
assert.deepEqual(body.verification_scope.remote_check.capabilities.request_modes, [
'images-non-stream',
'images-sse'
]);
assert.deepEqual(body.verification_scope.remote_check.runtime.effective_request_modes, [
'images-non-stream'
]);
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'GET /api/runtime-capabilities']
);
}
);
});
it('keeps default generate on server orchestration when request modes are declared', async () => {
const requests = [];
let imageRequestBody = '';
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify(
agentGenerateCapabilities({
supported: {
request_modes: [
'images-non-stream',
'images-sse',
'responses-non-stream',
'responses-sse'
]
},
upstream_request_headers: {
channels: [
{
id: 'images',
request_modes: ['images-non-stream', 'images-sse']
},
{
id: 'responses',
request_modes: ['responses-sse']
}
]
}
})
)
);
return;
}
if (request.url === '/api/agent/image-requests') {
imageRequestBody = await readRequestText(request);
response.writeHead(202, { 'content-type': 'application/json', 'retry-after': '1' });
response.end(
JSON.stringify({
job: {
id: 'job-orchestrated-1',
state: 'running',
result_url: '/api/agent/jobs/job-orchestrated-1/result',
retry_after_seconds: 1
}
})
);
return;
}
if (request.url === '/api/agent/jobs/job-orchestrated-1/result') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
request_id: 'job-orchestrated-1',
idempotency_key: 'orchestrated-key',
cached: false,
images: [
{
id: 'artifact-orchestrated-1',
filename: 'orchestrated.webp',
content_url: '/api/agent/artifacts/artifact-orchestrated-1/content',
width: 1254,
height: 1254
}
],
execution: {
transport: 'agent_job_polling',
endpoint: '/api/agent/image-requests',
route_mode: 'job',
image_backend: 'images-api',
stream_mode: 'non_stream',
streaming_strategy: 'off',
channel_request_mode: 'images-non-stream',
channel_request_mode_fallback_applied: false,
route_decision: {
requested_backend: 'images-api',
preferred_channel_request_mode: 'images-non-stream',
selected_channel_request_mode: 'images-non-stream',
fallback_applied: false,
selected_channel_id: 'channel-orchestrated',
upstream_host: 'upstream.example.test'
}
},
timing: { server_elapsed_ms: 1234 }
})
);
return;
}
if (request.url === '/api/agent/images/generate' || request.url === '/api/images') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected client-selected route' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--idempotency-key',
'orchestrated-key',
'--size',
'3072x2048',
'--streaming-strategy',
'off',
'--image-backend',
'responses',
'--responses-model',
'gpt-5.4-mini',
'--thinking',
'medium',
'--prompt-optimization',
'false',
'--force-web',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.deepEqual(body.routing, {
transport: 'server_orchestrated',
endpoint: '/api/agent/image-requests'
});
assert.equal(body.summary.transport, 'agent_job_polling');
assert.equal(body.summary.endpoint, '/api/agent/image-requests');
assert.equal(body.summary.route_mode, 'job');
assert.equal(body.summary.channel_request_mode, 'images-non-stream');
assert.equal(body.summary.channel_request_mode_fallback_applied, false);
assert.deepEqual(body.summary.route_decision, {
requested_backend: 'images-api',
preferred_channel_request_mode: 'images-non-stream',
selected_channel_request_mode: 'images-non-stream',
fallback_applied: false,
selected_channel_id: 'channel-orchestrated',
upstream_host: 'upstream.example.test'
});
assert.deepEqual(body.summary.content_urls, ['/api/agent/artifacts/artifact-orchestrated-1/content']);
assert.deepEqual(body.summary.actual_dimensions, { width: 1254, height: 1254 });
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /api/agent/capabilities',
'POST /api/agent/image-requests',
'GET /api/agent/jobs/job-orchestrated-1/result'
]
);
const requestBody = JSON.parse(imageRequestBody);
assert.equal(requestBody.size, '3072x2048');
assert.equal(requestBody.streaming_strategy, 'off');
assert.equal(requestBody.image_backend, 'responses');
assert.equal(requestBody.responsesModel, 'gpt-5.4-mini');
assert.equal(requestBody.thinking, 'medium');
assert.equal(requestBody.promptOptimization, false);
assert.equal(requestBody.force_web, true);
}
);
});
it('checks the server orchestration endpoint during generate contract checks', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify(
agentGenerateCapabilities({
agent_streaming: {
page_sse: {
supported: true,
endpoint: '/api/images',
client_request_id: { max_length: 128 }
}
}
})
)
);
return;
}
if (
request.url === '/api/agent/images/generate' ||
request.url === '/api/agent/image-requests' ||
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;
}
if (request.url === '/api/images') {
response.writeHead(400, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'clientRequestId 长度不能超过 128 个字符。' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync('generate-image.mjs', ['--contract-check', 'contract check'], {
GPT_IMAGE_PLAYGROUND_URL: baseUrl
});
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.deepEqual(
body.checks.map((check) => check.endpoint),
[
'/api/agent/images/generate',
'/api/agent/image-requests',
'/api/agent/jobs/images/generate',
'/api/images'
]
);
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /api/agent/capabilities',
'POST /api/agent/images/generate',
'POST /api/agent/image-requests',
'POST /api/agent/jobs/images/generate',
'POST /api/images'
]
);
}
);
});
it('fails generate contract checks when the default orchestration contract is missing', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify(agentGenerateCapabilities({ orchestration: undefined })));
return;
}
if (request.url === '/api/agent/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(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected contract probe' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync('generate-image.mjs', ['--contract-check', 'contract check'], {
GPT_IMAGE_PLAYGROUND_URL: baseUrl
});
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.error.code, 'orchestration_required');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/agent/images/generate']
);
}
);
});
it('expands generate presets into concrete dry-run request fields', () => {
const agentResult = runSkillScript('generate-image.mjs', ['--preset', '4k-agent-nonstream', 'prompt']);
assert.equal(agentResult.status, 0);
const agentBody = JSON.parse(agentResult.stdout);
assert.equal(agentBody.endpoint, 'http://localhost:4783/api/agent/images/generate');
assert.equal(agentBody.request.size, '3840x2160');
assert.equal(agentBody.request.quality, 'high');
assert.equal(agentBody.request.stream_mode, 'non_stream');
assert.equal(agentBody.routing_guidance.transport, 'agent_json');
const upstreamSseResult = runSkillScript('generate-image.mjs', [
'--preset',
'4k-upstream-sse-newapi',
'prompt'
]);
assert.equal(upstreamSseResult.status, 0);
const upstreamSseBody = JSON.parse(upstreamSseResult.stdout);
assert.equal(upstreamSseBody.request.size, '3840x2160');
assert.equal(upstreamSseBody.request.stream_mode, 'stream');
assert.equal(upstreamSseBody.request.streaming_strategy, 'newapi-keepalive-sse');
assert.equal(upstreamSseBody.request.partial_images, 2);
});
it('does not automatically fall back after an explicit billable page SSE generate failure', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end('data: {"type":"error","error":"stream failed","status":502}\n\n');
return;
}
if (request.url === '/api/agent/images/generate') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected fallback' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '3072x2048', '--quality', 'high', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.error.code, 'page_sse_failed');
assert.match(body.error.message, /stream failed/);
assert.match(body.next_step, /新的 Idempotency-Key/);
assert.match(body.next_step, /服务端编排入口/);
assert.equal(body.routing.fallback_endpoint, '/api/agent/image-requests');
assert.equal(body.routing.fallback_mode, 'manual_after_diagnosis');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/images']
);
}
);
});
it('reports non-SSE page validation failures as non-billable', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(400, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'quality 对 gpt-image-2 无效。' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '3072x2048', '--quality', 'invalid-quality', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.billable, false);
assert.equal(body.error.code, 'page_sse_request_rejected');
assert.equal(body.error.status, 400);
assert.match(body.error.message, /quality/);
assert.equal(body.routing.fallback_mode, 'fix_request_before_retry');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/images']
);
}
);
});
it('reports non-JSON page 4xx failures as non-billable request rejections', async () => {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(401, { 'content-type': 'text/plain' });
response.end('missing passwordHash');
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '3072x2048', '--quality', 'high', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.billable, false);
assert.equal(body.error.code, 'page_sse_request_rejected');
assert.equal(body.error.status, 401);
assert.match(body.error.message, /missing passwordHash/);
assert.equal(body.routing.fallback_mode, 'fix_request_before_retry');
}
);
});
it('reports non-SSE page server failures as billable page SSE failures', async () => {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: { message: 'upstream page failed' } }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '3072x2048', '--quality', 'high', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.billable, true);
assert.equal(body.error.code, 'page_sse_failed');
assert.equal(body.error.status, 500);
assert.match(body.error.message, /upstream page failed/);
assert.equal(body.routing.fallback_mode, 'manual_after_diagnosis');
}
);
});
it('preserves object page SSE error messages', async () => {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"error","error":{"message":"bad request from page","code":"validation_error","status":400}}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '3072x2048', '--quality', 'high', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.error.code, 'page_sse_failed');
assert.equal(body.billable, true);
assert.equal(body.error.status, 400);
assert.match(body.error.message, /bad request from page/);
assert.doesNotMatch(body.error.message, /\[object Object\]/);
}
);
});
it('fails large generate auto routing when page SSE capability is unavailable', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({ agent_streaming: {}, agent_jobs: { supported: true, mode: 'job_polling' } })
);
return;
}
if (request.url === '/api/agent/images/generate') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected fallback' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '3072x2048', '--quality', 'high', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.error.code, 'page_sse_unavailable');
assert.equal(body.routing.fallback_mode, 'manual_after_diagnosis');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities']
);
}
);
});
it('fails page SSE output when the stream ends before the done event', async () => {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"image.png","b64_json":"final-base64","path":"/api/image/image.png","output_format":"png"}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '3072x2048', '--quality', 'high', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.error.code, 'page_sse_failed');
assert.match(body.error.message, /缺少最终 done 事件/);
assert.equal(body.error.diagnostics.partial_image_count, 0);
assert.equal(body.error.diagnostics.completed_event_count, 1);
assert.equal(body.error.diagnostics.done_received, false);
assert.equal(body.error.diagnostics.final_image_count, 1);
assert.equal(body.error.diagnostics.last_upstream_event_type, 'completed');
}
);
});
it('reports page SSE partial image diagnostics when no final image arrives', async () => {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({ agent_streaming: { page_sse: { supported: true, endpoint: '/api/images' } } })
);
return;
}
if (request.url === '/api/images') {
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"image_generation.partial_image","partial_image_b64":"partial-a"}',
'',
'data: {"type":"done","images":[]}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '1024x1024', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.error.code, 'page_sse_failed');
assert.match(body.error.message, /未返回最终图片/);
assert.equal(body.error.diagnostics.partial_image_count, 1);
assert.equal(body.error.diagnostics.completed_event_count, 0);
assert.equal(body.error.diagnostics.done_received, true);
assert.equal(body.error.diagnostics.final_image_count, 0);
assert.equal(body.error.diagnostics.last_upstream_event_type, 'done');
}
);
});
it('saves raw page SSE events when a generate log path is configured', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-sse-log-'));
try {
const logPath = join(tempRoot, 'events.jsonl');
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
}
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'event: image_generation.partial_image',
'data: {"type":"image_generation.partial_image","partial_image_b64":"abc"}',
'',
'data: {"type":"completed","filename":"image.png","path":"/generated/image.png"}',
'',
'data: {"type":"done","images":[{"filename":"image.png","path":"/generated/image.png"}]}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--sse-log', logPath, '--size', '1024x1024', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const logLines = readFileSync(logPath, 'utf8').trim().split(/\r?\n/).map(JSON.parse);
assert.equal(logLines.length, 5);
assert.equal(logLines[0].event, 'request_started');
assert.equal(logLines[0].client_request_id.startsWith('agent-generate-'), true);
assert.match(logLines[1].raw_event, /partial_image/);
assert.match(logLines[3].raw_event, /"type":"done"/);
assert.equal(logLines[4].event, 'request_completed');
assert.equal(typeof logLines[4].elapsed_ms, 'number');
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('keeps generate page SSE successful when the optional raw log path is unwritable', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-sse-log-unwritable-'));
try {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
}
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"image.png","path":"/generated/image.png"}',
'',
'data: {"type":"done","images":[{"filename":"image.png","path":"/generated/image.png"}]}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--sse-log', tempRoot, '--size', '1024x1024', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.match(result.stderr, /SSE log write failed/);
const body = JSON.parse(result.stdout);
assert.equal(body.images.length, 1);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('requires page access hash before calling page SSE when capabilities declare page auth', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: {
supported: true,
endpoint: '/api/images',
auth: {
required: true,
schemes: ['form-password-hash'],
form_field: 'passwordHash'
}
}
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected page call' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '3072x2048', '--quality', 'high', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.error.code, 'page_sse_auth_required');
assert.match(body.error.message, /GPT_IMAGE_APP_PASSWORD_HASH/);
assert.match(body.error.message, /\.env\.agent\.local/);
assert.match(body.error.message, /GPT_IMAGE_AGENT_TOKEN/);
assert.equal(body.summary.ok, false);
assert.equal(body.summary.billable, false);
assert.equal(body.summary.request_id, null);
assert.equal(body.summary.idempotency_key.startsWith('agent-generate-'), true);
assert.deepEqual(body.summary.artifact_ids, []);
assert.deepEqual(body.summary.content_urls, []);
assert.deepEqual(body.summary.absolute_content_urls, []);
assert.equal(body.summary.route_mode, 'page_sse');
assert.equal(body.summary.image_backend, null);
assert.equal(body.summary.stream_mode, null);
assert.equal(body.summary.streaming_strategy, null);
assert.equal(body.summary.selected_channel_id, null);
assert.equal(body.summary.upstream_host, null);
assert.equal(body.summary.transport_error_kind, null);
assert.equal(body.summary.retry_after_ms, null);
assert.equal(body.summary.retry_after_seconds, null);
assert.equal(body.summary.cooldown_until, null);
assert.equal(body.summary.cooldown_target, null);
assert.equal(body.summary.elapsed_source, 'client_script');
assert.equal(typeof body.summary.elapsed_breakdown.client_script_ms, 'number');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities']
);
}
);
});
it('rejects overlong page SSE client request ids before sending the stream request', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: {
supported: true,
endpoint: '/api/images',
auth: { required: false, schemes: [], form_field: 'passwordHash' }
}
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected page call' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '3072x2048', '--quality', 'high', 'prompt'],
{
GPT_IMAGE_PLAYGROUND_URL: baseUrl,
GPT_IMAGE_AGENT_IDEMPOTENCY_KEY: 'x'.repeat(129)
}
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.error.code, 'page_sse_client_request_id_too_long');
assert.match(body.error.message, /不能超过 128 个字符/);
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities']
);
}
);
});
it('enriches failed Agent generate summaries with Agent state diagnostics', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/images/generate') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
error: { code: 'unexpected_error', message: 'fetch failed', retryable: false }
})
);
return;
}
if (request.url === '/api/agent/diagnostics/requests?idempotency_key=diag-generate-key') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
found: true,
diagnostics: {
request: {
request_id: 'req_generate_diag',
idempotency_key: 'diag-generate-key',
status: 'failed'
},
error: {
code: 'unexpected_error',
retryable: false,
diagnostics: {
channel_request_mode: 'images-non-stream',
channel_request_mode_fallback_applied: true,
route_decision: {
requested_backend: 'images-api',
preferred_channel_request_mode: 'images-sse',
fallback_channel_request_mode: 'images-non-stream',
selected_channel_request_mode: 'images-non-stream',
fallback_applied: true,
selected_channel_id: 'channel-a',
upstream_host: 'upstream.example.test'
},
selected_channel_id: 'channel-a',
upstream_host: 'upstream.example.test',
transport_error_kind: 'aborted',
retry_after_ms: 30000,
cooldown_until: '2026-06-11T12:00:00.000Z',
cooldown_target: {
channel_id: 'channel-a',
request_mode: 'images-non-stream'
}
}
}
}
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--agent',
'--idempotency-key',
'diag-generate-key',
'--size',
'1024x1024',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.summary.request_id, 'req_generate_diag');
assert.equal(body.summary.channel_request_mode, 'images-non-stream');
assert.equal(body.summary.channel_request_mode_fallback_applied, true);
assert.deepEqual(body.summary.route_decision, {
requested_backend: 'images-api',
preferred_channel_request_mode: 'images-sse',
fallback_channel_request_mode: 'images-non-stream',
selected_channel_request_mode: 'images-non-stream',
fallback_applied: true,
selected_channel_id: 'channel-a',
upstream_host: 'upstream.example.test'
});
assert.equal(body.summary.selected_channel_id, 'channel-a');
assert.equal(body.summary.upstream_host, 'upstream.example.test');
assert.equal(body.summary.transport_error_kind, 'aborted');
assert.equal(body.summary.retry_after_ms, 30000);
assert.equal(body.summary.cooldown_until, '2026-06-11T12:00:00.000Z');
assert.deepEqual(body.summary.cooldown_target, {
channel_id: 'channel-a',
request_mode: 'images-non-stream'
});
assert.equal(body.summary.agent_diagnostics_checked, true);
assert.equal(body.summary.agent_diagnostics_found, true);
assert.equal(body.agent_failure_diagnostics.request_id, 'req_generate_diag');
assert.equal(body.agent_failure_diagnostics.status, 'failed');
assert.deepEqual(body.agent_failure_diagnostics.cooldown_target, {
channel_id: 'channel-a',
request_mode: 'images-non-stream'
});
assert.deepEqual(body.agent_failure_diagnostics.route_decision, {
requested_backend: 'images-api',
preferred_channel_request_mode: 'images-sse',
fallback_channel_request_mode: 'images-non-stream',
selected_channel_request_mode: 'images-non-stream',
fallback_applied: true,
selected_channel_id: 'channel-a',
upstream_host: 'upstream.example.test'
});
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /api/agent/capabilities',
'POST /api/agent/images/generate',
'GET /api/agent/diagnostics/requests?idempotency_key=diag-generate-key'
]
);
}
);
});
it('preserves path prefixes when fetching Agent state diagnostics', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/playground/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/playground/api/agent/images/generate') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
error: { code: 'unexpected_error', message: 'fetch failed', retryable: false }
})
);
return;
}
if (request.url === '/playground/api/agent/diagnostics/requests?idempotency_key=path-diag-key') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
found: true,
diagnostics: {
request: {
request_id: 'req_path_diag',
idempotency_key: 'path-diag-key',
status: 'failed'
}
}
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--agent',
'--idempotency-key',
'path-diag-key',
'--size',
'1024x1024',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: `${baseUrl}/playground` }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.summary.request_id, 'req_path_diag');
assert.equal(body.summary.agent_diagnostics_checked, true);
assert.equal(body.summary.agent_diagnostics_found, true);
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /playground/api/agent/capabilities',
'POST /playground/api/agent/images/generate',
'GET /playground/api/agent/diagnostics/requests?idempotency_key=path-diag-key'
]
);
}
);
});
it('replaces stale retry guidance when Agent diagnostics report a terminal failure', async () => {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/images/generate') {
response.writeHead(503, { 'content-type': 'application/json', 'retry-after': '1' });
response.end(
JSON.stringify({
error: { code: 'network_error', message: 'temporary failure', retryable: true }
})
);
return;
}
if (request.url === '/api/agent/diagnostics/requests?idempotency_key=terminal-diag-key') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
found: true,
diagnostics: {
request: {
request_id: 'req_terminal_diag',
idempotency_key: 'terminal-diag-key',
status: 'failed'
},
error: {
code: 'upstream_failed',
retryable: false
}
}
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--agent',
'--idempotency-key',
'terminal-diag-key',
'--size',
'1024x1024',
'prompt'
],
{
GPT_IMAGE_AGENT_MAX_ATTEMPTS: '1',
GPT_IMAGE_PLAYGROUND_URL: baseUrl
}
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.summary.request_id, 'req_terminal_diag');
assert.equal(body.summary.retryable, false);
assert.equal(body.summary.next_action, 'diagnose_then_new_idempotency_key');
assert.equal(body.agent_failure_diagnostics.retryable, false);
}
);
});
it('does not report diagnostics lookup failures as upstream transport errors', async () => {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/images/generate') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
error: { code: 'unexpected_error', message: 'fetch failed', retryable: false }
})
);
return;
}
if (request.url === '/api/agent/diagnostics/requests?idempotency_key=diag-unavailable-key') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'diagnostics unavailable' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--agent',
'--idempotency-key',
'diag-unavailable-key',
'--size',
'1024x1024',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.summary.transport_error_kind, null);
assert.equal(body.summary.agent_diagnostics_checked, true);
assert.equal(body.summary.agent_diagnostics_found, false);
assert.equal(body.summary.agent_diagnostics_unavailable_reason, 'status_500');
assert.equal(body.agent_failure_diagnostics.unavailable_reason, 'status_500');
}
);
});
it('uses page SSE client request id max length declared by capabilities', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: {
supported: true,
endpoint: '/api/images',
auth: { required: false, schemes: [], form_field: 'passwordHash' },
client_request_id: { max_length: 12 }
}
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected page call' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--page-sse',
'--size',
'3072x2048',
'--quality',
'high',
'--idempotency-key',
'too-long-page-key',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.error.code, 'page_sse_client_request_id_too_long');
assert.match(body.error.message, /不能超过 12 个字符/);
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities']
);
}
);
});
it('checks default path-mode page SSE generate dimensions from raw SSE image bytes before formatting', async () => {
const finalImageBase64 = fakePngBase64(3072, 2048);
let imageRouteHits = 0;
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: {
supported: true,
endpoint: '/api/images',
auth: { required: false, schemes: [], form_field: 'passwordHash' }
}
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
`data: ${JSON.stringify({
type: 'completed',
filename: 'image.png',
b64_json: finalImageBase64,
path: '/api/image/image.png',
output_format: 'png'
})}`,
'',
'data: {"type":"done","client_request_id":"page-request-1","images":[{"filename":"image.png"}]}',
'',
''
].join('\n')
);
return;
}
if (request.url === '/api/image/image.png') {
imageRouteHits += 1;
response.writeHead(401, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'cookie required' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--page-sse',
'--dimension-check',
'--size',
'3072x2048',
'--quality',
'high',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
assert.equal(imageRouteHits, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.images[0].filename, 'image.png');
assert.equal('b64_json' in body.images[0], false);
assert.equal(body.images[0].path, '/api/image/image.png');
assert.equal(body.images[0].absolute_path, `${baseUrl}/api/image/image.png`);
assert.equal(body.images[0].output_format, 'png');
assert.equal(body.images[0].clientRequestId, 'page-request-1');
assert.deepEqual(body.images[0].dimensions, { width: 3072, height: 2048 });
assert.deepEqual(body.summary.actual_dimensions, { width: 3072, height: 2048 });
}
);
});
it('passes response mode through to page SSE form-data requests', async () => {
let pageRequestBody = '';
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: {
supported: true,
endpoint: '/api/images',
auth: { required: false, schemes: [], form_field: 'passwordHash' }
}
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
pageRequestBody = await readRequestText(request);
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"image.png","b64_json":"final-base64","output_format":"png"}',
'',
'data: {"type":"done","client_request_id":"page-request-response-mode"}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--page-sse',
'--size',
'3072x2048',
'--quality',
'high',
'--response-mode',
'base64',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
assert.match(pageRequestBody, /name="response_mode"\r?\n\r?\nbase64/);
}
);
});
it('omits implicit page streaming controls unless explicitly requested', async () => {
let pageRequestBody = '';
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: {
supported: true,
endpoint: '/api/images',
auth: { required: false, schemes: [], form_field: 'passwordHash' }
}
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
pageRequestBody = await readRequestText(request);
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"image.png","b64_json":"final-base64","output_format":"png"}',
'',
'data: {"type":"done","client_request_id":"page-request-default-streaming"}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '1024x1024', '--quality', 'high', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
assert.doesNotMatch(pageRequestBody, /name="stream"/);
assert.doesNotMatch(pageRequestBody, /name="image_streaming_strategy"/);
}
);
});
it('enforces timeout while waiting for page SSE body events', async () => {
let pageSseRequestReceived = false;
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: {
supported: true,
endpoint: '/api/images',
auth: { required: false, schemes: [], form_field: 'passwordHash' }
}
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
pageSseRequestReceived = true;
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.write(': keepalive\n\n');
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--page-sse',
'--timeout-ms',
'1000',
'--size',
'1024x1024',
'--quality',
'high',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl },
{ timeoutMs: 10_000 }
);
assert.equal(result.timedOut, false);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
assert.equal(pageSseRequestReceived, true);
const body = JSON.parse(result.stderr);
assert.equal(body.error.code, 'page_sse_failed');
assert.match(body.error.message, /\/api\/images/);
}
);
});
it('passes page access hash through to page SSE form-data requests when configured', async () => {
let pageRequestBody = '';
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: {
supported: true,
endpoint: '/api/images',
auth: {
required: true,
schemes: ['form-password-hash'],
form_field: 'passwordHash'
}
}
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
pageRequestBody = await readRequestText(request);
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"image.png","b64_json":"final-base64","output_format":"png"}',
'',
'data: {"type":"done","client_request_id":"page-request-password"}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '3072x2048', '--quality', 'high', 'prompt'],
{
GPT_IMAGE_PLAYGROUND_URL: baseUrl,
GPT_IMAGE_APP_PASSWORD_HASH: 'hash-for-page-sse'
}
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
assert.match(pageRequestBody, /name="passwordHash"\r?\n\r?\nhash-for-page-sse/);
}
);
});
it('keeps page SSE base64 in default path-mode output when no path is returned', async () => {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: {
supported: true,
endpoint: '/api/images',
auth: { required: false, schemes: [], form_field: 'passwordHash' }
}
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"image.png","b64_json":"indexeddb-base64","output_format":"png"}',
'',
'data: {"type":"done","client_request_id":"page-request-indexeddb"}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '3072x2048', '--quality', 'high', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.images[0].filename, 'image.png');
assert.equal(body.images[0].b64_json, 'indexeddb-base64');
assert.equal('absolute_path' in body.images[0], false);
assert.equal(body.images[0].clientRequestId, 'page-request-indexeddb');
}
);
});
it('preserves completed page SSE images when the done event lists fewer images', async () => {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: {
supported: true,
endpoint: '/api/images',
auth: { required: false, schemes: [], form_field: 'passwordHash' }
}
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"image-a.png","b64_json":"base64-a","path":"/api/image/image-a.png","output_format":"png"}',
'',
'data: {"type":"completed","filename":"image-b.png","b64_json":"base64-b","path":"/api/image/image-b.png","output_format":"png"}',
'',
'data: {"type":"done","client_request_id":"page-request-2","images":[{"filename":"image-a.png"}]}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '3072x2048', '--quality', 'high', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.images.length, 2);
assert.equal(body.images[0].filename, 'image-a.png');
assert.equal(body.images[1].filename, 'image-b.png');
assert.equal(body.images[1].path, '/api/image/image-b.png');
assert.equal(body.images[1].absolute_path, `${baseUrl}/api/image/image-b.png`);
assert.equal(body.images[1].clientRequestId, 'page-request-2');
}
);
});
it('passes streaming strategy off through explicit Agent JSON generate requests', async () => {
const requests = [];
let agentRequestBody = '';
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/agent/images/generate') {
agentRequestBody = await readRequestText(request);
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
images: [
{
filename: 'agent-off.png',
content_url: '/api/agent/artifacts/artifact-off/content',
metadata_url: '/api/agent/artifacts/artifact-off',
width: 1254,
height: 1254
}
],
timing: { server_elapsed_ms: 4321 }
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected page SSE call' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--agent',
'--size',
'3072x2048',
'--quality',
'high',
'--streaming-strategy',
'off',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.images[0].filename, 'agent-off.png');
assert.deepEqual(body.routing, { transport: 'agent_json', endpoint: '/api/agent/images/generate' });
assert.equal(body.summary.ok, true);
assert.equal(body.summary.billable, true);
assert.equal(body.summary.idempotency_key.startsWith('agent-generate-'), true);
assert.equal(body.summary.transport, 'agent_json');
assert.equal(body.summary.endpoint, '/api/agent/images/generate');
assert.equal(body.summary.route_mode, 'agent');
assert.deepEqual(body.summary.content_urls, ['/api/agent/artifacts/artifact-off/content']);
assert.deepEqual(body.summary.absolute_content_urls, [
`${baseUrl}/api/agent/artifacts/artifact-off/content`
]);
assert.deepEqual(body.summary.actual_dimensions, { width: 1254, height: 1254 });
assert.deepEqual(body.summary.image_dimensions, [{ width: 1254, height: 1254 }]);
assert.equal(typeof body.summary.elapsed_ms, 'number');
assert.equal(body.summary.elapsed_source, 'client_script');
assert.equal(body.summary.server_elapsed_ms, 4321);
assert.equal(body.summary.elapsed_breakdown.upstream_or_server_ms, 4321);
assert.equal(body.summary.next_action, 'done');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/agent/images/generate']
);
const requestBody = JSON.parse(agentRequestBody);
assert.equal(requestBody.size, '3072x2048');
assert.equal(requestBody.streaming_strategy, 'off');
}
);
});
it('uses Agent JSON for billable large generate requests when --agent is explicit', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/agent/images/generate') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
images: [
{
filename: 'agent.png',
content_url: '/api/agent/artifacts/artifact-1/content',
metadata_url: '/api/agent/artifacts/artifact-1'
}
]
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected page SSE call' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--agent', '--size', '3072x2048', '--quality', 'high', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.images[0].filename, 'agent.png');
assert.equal(body.images[0].absolute_content_url, `${baseUrl}/api/agent/artifacts/artifact-1/content`);
assert.deepEqual(body.routing, { transport: 'agent_json', endpoint: '/api/agent/images/generate' });
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/agent/images/generate']
);
}
);
});
it('uses page SSE for billable small generate requests when --page-sse is explicit', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"small-page.png","path":"/api/image/small-page.png","output_format":"png"}',
'',
'data: {"type":"done","client_request_id":"small-page-request","images":[{"filename":"small-page.png"}]}',
'',
''
].join('\n')
);
return;
}
if (request.url === '/api/agent/images/generate') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected Agent JSON call' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--size', '1024x1024', '--quality', 'high', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.images[0].filename, 'small-page.png');
assert.equal(body.images[0].absolute_path, `${baseUrl}/api/image/small-page.png`);
assert.deepEqual(body.routing, {
transport: 'page_sse',
endpoint: '/api/images',
route_mode: 'page_sse',
fallback_endpoint: '/api/agent/image-requests',
fallback_mode: 'manual_after_diagnosis',
stream_mode: null,
streaming_strategy: null
});
assert.equal(body.summary.ok, true);
assert.equal(body.summary.billable, true);
assert.equal(body.summary.transport, 'page_sse');
assert.equal(body.summary.endpoint, '/api/images');
assert.equal(body.summary.route_mode, 'page_sse');
assert.deepEqual(body.summary.content_urls, ['/api/image/small-page.png']);
assert.deepEqual(body.summary.absolute_content_urls, [`${baseUrl}/api/image/small-page.png`]);
assert.equal(typeof body.summary.elapsed_ms, 'number');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/images']
);
}
);
});
it('passes GPT2Image-compatible generate options to page SSE only when explicitly requested', async () => {
const requests = [];
let pageSseRequestBody = '';
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ agent_streaming: { page_sse: { supported: true } } }));
return;
}
if (request.url === '/api/images') {
pageSseRequestBody = await readRequestText(request);
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"generate.jpg","path":"/generated/generate.jpg"}',
'',
'data: {"type":"done","images":[{"filename":"generate.jpg","path":"/generated/generate.jpg"}]}',
'',
''
].join('\n')
);
return;
}
if (request.url === '/api/agent/images/generate') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected Agent generate call' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--page-sse',
'--image-backend',
'responses',
'--responses-model',
'gpt-5.4-mini',
'--thinking',
'medium',
'--prompt-optimization',
'false',
'--force-web',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/images']
);
assert.match(pageSseRequestBody, /name="mode"\r?\n\r?\ngenerate/);
assert.match(pageSseRequestBody, /name="image_backend"\r?\n\r?\nresponses-image-generation/);
assert.match(pageSseRequestBody, /name="responsesModel"\r?\n\r?\ngpt-5\.4-mini/);
assert.match(pageSseRequestBody, /name="thinking"\r?\n\r?\nmedium/);
assert.match(pageSseRequestBody, /name="promptOptimization"\r?\n\r?\nfalse/);
assert.match(pageSseRequestBody, /name="force_web"\r?\n\r?\ntrue/);
}
);
});
it('rejects generate page SSE partial images outside the selected backend capability before POST', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: { page_sse: { supported: true } },
defaults: { partial_images: 2 },
limits: {
partial_images: { min: 0, max: 4 },
partial_images_by_backend: {
'images-api': { min: 0, max: 4 },
'responses-image-generation': { min: 1, max: 3 }
}
}
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'page SSE should not be called' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--image-backend',
'responses',
'--responses-model',
'gpt-5.4-mini',
'--partial-images',
'0',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 2);
assert.equal(result.stdout.trim(), '');
assert.match(result.stderr, /partial_images 必须在当前 capabilities 允许的 1 到 3 之间/);
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities']
);
}
);
});
it('rejects explicit page SSE when stream-mode is non_stream before network requests', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected request' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--page-sse', '--stream-mode', 'non_stream', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 2);
assert.equal(result.stdout.trim(), '');
assert.match(result.stderr, /stream_mode=non_stream/);
assert.doesNotMatch(result.stderr, /ModuleJob|at buildGenerateRoutingGuidance/);
assert.deepEqual(requests, []);
}
);
});
it('passes generate response controls through explicit Agent and job diagnostic routes', async () => {
const requests = [];
let agentRequestBody = '';
let jobRequestBody = '';
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify(agentGenerateCapabilities()));
return;
}
if (request.url === '/api/agent/images/generate') {
agentRequestBody = await readRequestText(request);
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
images: [
{
id: 'agent-artifact',
filename: 'agent.webp',
content_url: '/api/agent/artifacts/agent-artifact/content'
}
]
})
);
return;
}
if (request.url === '/api/agent/jobs/images/generate') {
jobRequestBody = await readRequestText(request);
response.writeHead(202, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
job: {
id: 'job-diagnostic-1',
state: 'running',
result_url: '/api/agent/jobs/job-diagnostic-1/result',
retry_after_seconds: 1
}
})
);
return;
}
if (request.url === '/api/agent/jobs/job-diagnostic-1/result') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
request_id: 'job-diagnostic-1',
idempotency_key: 'job-diagnostic-key',
cached: false,
images: [
{
id: 'job-artifact',
filename: 'job.webp',
content_url: '/api/agent/artifacts/job-artifact/content'
}
],
execution: {
transport: 'agent_job_polling',
endpoint: '/api/agent/jobs/images/generate',
route_mode: 'job',
image_backend: 'responses-image-generation',
stream_mode: 'auto',
streaming_strategy: 'auto'
}
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--agent',
'--image-backend',
'responses',
'--responses-model',
'gpt-5.4-mini',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const agentBody = JSON.parse(agentRequestBody);
assert.equal(agentBody.image_backend, 'responses');
assert.equal(agentBody.responsesModel, 'gpt-5.4-mini');
const jobResult = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--job',
'--idempotency-key',
'job-diagnostic-key',
'--image-backend',
'responses',
'--responses-model',
'gpt-5.4-mini',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(jobResult.status, 0);
assert.equal(jobResult.stderr.trim(), '');
const jobBody = JSON.parse(jobRequestBody);
assert.equal(jobBody.image_backend, 'responses');
assert.equal(jobBody.responsesModel, 'gpt-5.4-mini');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /api/agent/capabilities',
'POST /api/agent/images/generate',
'GET /api/agent/capabilities',
'POST /api/agent/jobs/images/generate',
'GET /api/agent/jobs/job-diagnostic-1/result'
]
);
}
);
});
it('keeps generate response controls on server orchestration when streaming is disabled', async () => {
const requests = [];
let imageRequestBody = '';
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify(agentGenerateCapabilities()));
return;
}
if (request.url === '/api/agent/image-requests') {
imageRequestBody = await readRequestText(request);
response.writeHead(202, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
job: {
id: 'job-nonstream-controls',
state: 'running',
result_url: '/api/agent/jobs/job-nonstream-controls/result',
retry_after_seconds: 1
}
})
);
return;
}
if (request.url === '/api/agent/jobs/job-nonstream-controls/result') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
request_id: 'job-nonstream-controls',
idempotency_key: 'nonstream-controls-key',
cached: false,
images: [
{
id: 'artifact-nonstream-controls',
filename: 'nonstream.webp',
content_url: '/api/agent/artifacts/artifact-nonstream-controls/content'
}
],
execution: {
transport: 'agent_job_polling',
endpoint: '/api/agent/image-requests',
route_mode: 'job',
image_backend: 'responses-image-generation',
stream_mode: 'non_stream',
streaming_strategy: 'auto'
}
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--idempotency-key',
'nonstream-controls-key',
'--image-backend',
'responses',
'--responses-model',
'gpt-5.4-mini',
'--stream-mode',
'non_stream',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const requestBody = JSON.parse(imageRequestBody);
assert.equal(requestBody.image_backend, 'responses');
assert.equal(requestBody.responsesModel, 'gpt-5.4-mini');
assert.equal(requestBody.stream_mode, 'non_stream');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /api/agent/capabilities',
'POST /api/agent/image-requests',
'GET /api/agent/jobs/job-nonstream-controls/result'
]
);
}
);
});
it('prints page SSE guidance for high-resolution edit dry-runs', () => {
const result = runSkillScript('edit-image.mjs', [
'--size',
'3072x2048',
'--quality',
'high',
'/tmp/source.png',
'prompt'
]);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.routing_guidance.recommended_endpoint, '/api/images');
assert.equal(body.routing_guidance.transport, 'page_sse');
assert.equal(body.routing_guidance.strength, 'default');
assert.equal(result.stderr.trim(), '');
});
it('rejects edit dimension-check without a fixed requested size before remote execution', () => {
const result = runSkillScript('edit-image.mjs', [
'--dimension-check',
'--size',
'auto',
'/tmp/source.png',
'prompt'
]);
assert.equal(result.status, 2);
assert.equal(result.stdout.trim(), '');
assert.match(result.stderr, /--dimension-check 需要 --size 为 WIDTHxHEIGHT/);
});
it('routes GPT2Image-compatible edit options through page SSE dry-runs', () => {
const result = runSkillScript('edit-image.mjs', [
'--format',
'jpeg',
'--output-compression',
'85',
'--moderation',
'auto',
'--image-backend',
'responses',
'--responses-model',
'gpt-5.4-mini',
'--thinking',
'medium',
'--prompt-optimization',
'false',
'--force-web',
'/tmp/source.png',
'prompt'
]);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.routing_guidance.transport, 'page_sse');
assert.equal(body.request.output_format, 'jpeg');
assert.equal(body.request.output_compression, 85);
assert.equal(body.request.moderation, 'auto');
assert.equal(body.request.image_backend, 'responses-image-generation');
assert.equal(body.request.responsesModel, 'gpt-5.4-mini');
assert.equal(body.request.thinking, 'medium');
assert.equal(body.request.promptOptimization, false);
assert.equal(body.request.force_web, true);
});
it('shows edit dimension-check in dry-run requests', () => {
const result = runSkillScript('edit-image.mjs', [
'--dimension-check',
'--size',
'1024x1024',
'--agent',
'/tmp/source.png',
'prompt'
]);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.request.dimension_check, true);
assert.equal(body.routing_guidance.transport, 'agent_json');
});
it('uses page SSE for billable high-resolution edit requests', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-edit-page-sse-'));
try {
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
const requests = [];
let pageSseRequestBody = '';
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
}
})
);
return;
}
if (request.url === '/api/images') {
pageSseRequestBody = await readRequestText(request);
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"edit.png","path":"/generated/edit.png","clientRequestId":"edit-page-sse-key"}',
'',
'data: {"type":"done","images":[{"filename":"edit.png","path":"/generated/edit.png"}],"clientRequestId":"edit-page-sse-key"}',
'',
''
].join('\n')
);
return;
}
if (request.url === '/api/agent/images/edit') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected Agent edit call' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'edit-image.mjs',
[
'--allow-billable',
'--size',
'3072x2048',
'--idempotency-key',
'edit-page-sse-key',
imagePath,
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.deepEqual(body.routing, {
transport: 'page_sse',
endpoint: '/api/images',
fallback_endpoint: '/api/agent/images/edit',
fallback_mode: 'manual_after_diagnosis'
});
assert.equal(body.images[0].absolute_path, `${baseUrl}/generated/edit.png`);
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/images']
);
assert.match(pageSseRequestBody, /name="mode"\r?\n\r?\nedit/);
assert.match(pageSseRequestBody, /name="clientRequestId"\r?\n\r?\nedit-page-sse-key/);
assert.match(pageSseRequestBody, /name="image_0"; filename="source\.png"/);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('checks edit page SSE output dimensions from raw SSE image bytes before formatting', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-edit-page-sse-dim-'));
try {
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
const finalImageBase64 = fakePngBase64(1024, 1024);
let imageRouteHits = 0;
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify(
agentGenerateCapabilities({ agent_streaming: { page_sse: { supported: true } } })
)
);
return;
}
if (request.url === '/api/images') {
await readRequestText(request);
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
`data: ${JSON.stringify({
type: 'completed',
filename: 'edit.png',
b64_json: finalImageBase64,
path: '/api/image/edit-ok.png'
})}`,
'',
'data: {"type":"done","images":[{"filename":"edit.png","path":"/api/image/edit-ok.png"}]}',
'',
''
].join('\n')
);
return;
}
if (request.url === '/api/image/edit-ok.png') {
imageRouteHits += 1;
response.writeHead(401, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'cookie required' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'edit-image.mjs',
[
'--allow-billable',
'--dimension-check',
'--page-sse',
'--size',
'1024x1024',
imagePath,
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
assert.equal(imageRouteHits, 0);
const body = JSON.parse(result.stdout);
assert.equal('b64_json' in body.images[0], false);
assert.equal(body.images[0].path, '/api/image/edit-ok.png');
assert.equal(body.images[0].absolute_path, `${baseUrl}/api/image/edit-ok.png`);
assert.deepEqual(body.images[0].dimensions, { width: 1024, height: 1024 });
assert.deepEqual(body.summary.actual_dimensions, { width: 1024, height: 1024 });
assert.deepEqual(body.summary.image_dimensions, [{ width: 1024, height: 1024 }]);
assert.equal(body.summary.transport, 'page_sse');
assert.equal(body.summary.endpoint, '/api/images');
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('reports edit dimension-check artifact download failures as validation failures', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-edit-page-sse-dim-download-'));
try {
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify(
agentGenerateCapabilities({ agent_streaming: { page_sse: { supported: true } } })
)
);
return;
}
if (request.url === '/api/images') {
await readRequestText(request);
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"edit.png","path":"/artifact/missing-edit.png"}',
'',
'data: {"type":"done","images":[{"filename":"edit.png","path":"/artifact/missing-edit.png"}]}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing artifact' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'edit-image.mjs',
[
'--allow-billable',
'--dimension-check',
'--page-sse',
'--size',
'1024x1024',
imagePath,
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.error.code, 'dimension_check_failed');
assert.match(body.error.message, /下载产物失败,状态码 404/);
assert.equal(body.validation_failure_kind, 'generated_artifact_failed_dimension_check');
assert.equal(body.summary.dimension_check_failed, true);
assert.deepEqual(body.summary.expected_dimensions, { width: 1024, height: 1024 });
assert.equal(body.summary.actual_dimensions, null);
assert.equal(body.summary.transport, 'page_sse');
assert.equal(body.response.images[0].absolute_path, `${baseUrl}/artifact/missing-edit.png`);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('passes GPT2Image-compatible edit options to page SSE form-data', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-edit-page-sse-fields-'));
try {
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
let pageSseRequestBody = '';
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ agent_streaming: { page_sse: { supported: true } } }));
return;
}
if (request.url === '/api/images') {
pageSseRequestBody = await readRequestText(request);
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"edit.jpg","path":"/generated/edit.jpg"}',
'',
'data: {"type":"done","images":[{"filename":"edit.jpg","path":"/generated/edit.jpg"}]}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'edit-image.mjs',
[
'--allow-billable',
'--format',
'jpeg',
'--output-compression',
'85',
'--moderation',
'auto',
'--image-backend',
'responses',
'--responses-model',
'gpt-5.4-mini',
'--thinking',
'medium',
'--prompt-optimization',
'false',
'--force-web',
imagePath,
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
assert.match(pageSseRequestBody, /name="output_format"\r?\n\r?\njpeg/);
assert.match(pageSseRequestBody, /name="output_compression"\r?\n\r?\n85/);
assert.match(pageSseRequestBody, /name="moderation"\r?\n\r?\nauto/);
assert.match(pageSseRequestBody, /name="image_backend"\r?\n\r?\nresponses-image-generation/);
assert.match(pageSseRequestBody, /name="responsesModel"\r?\n\r?\ngpt-5\.4-mini/);
assert.match(pageSseRequestBody, /name="thinking"\r?\n\r?\nmedium/);
assert.match(pageSseRequestBody, /name="promptOptimization"\r?\n\r?\nfalse/);
assert.match(pageSseRequestBody, /name="force_web"\r?\n\r?\ntrue/);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('keeps edit page SSE successful when the optional raw log path is unwritable', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-edit-sse-log-unwritable-'));
try {
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ agent_streaming: { page_sse: { supported: true } } }));
return;
}
if (request.url === '/api/images') {
await readRequestText(request);
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"edit.png","path":"/generated/edit.png"}',
'',
'data: {"type":"done","images":[{"filename":"edit.png","path":"/generated/edit.png"}]}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'edit-image.mjs',
['--allow-billable', '--page-sse', '--sse-log', tempRoot, imagePath, 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.match(result.stderr, /SSE log write failed/);
const body = JSON.parse(result.stdout);
assert.equal(body.images.length, 1);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('uses Agent edit for explicit high-resolution edit fallback requests', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-edit-agent-fallback-'));
try {
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
const requests = [];
let editRequestBody = '';
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
}
})
);
return;
}
if (request.url === '/api/agent/images/edit') {
editRequestBody = await readRequestText(request);
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ images: [{ id: 'edit-image', filename: 'edit.png' }] }));
return;
}
if (request.url === '/api/images') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected page SSE call' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'edit-image.mjs',
['--allow-billable', '--agent', '--size', '3072x2048', imagePath, 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/agent/images/edit']
);
assert.match(editRequestBody, /name="size"\r?\n\r?\n3072x2048/);
assert.match(editRequestBody, /name="image_0"; filename="source\.png"/);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('checks explicit Agent edit output dimensions from base64 images', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-edit-agent-dim-'));
try {
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify(agentGenerateCapabilities()));
return;
}
if (request.url === '/api/agent/images/edit') {
await readRequestText(request);
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
request_id: 'edit-dimension-ok',
idempotency_key: 'edit-dimension-ok-key',
execution: {
transport: 'agent_json',
endpoint: '/api/agent/images/edit',
route_mode: 'agent',
channel_request_mode: 'images-non-stream',
channel_request_mode_fallback_applied: false,
route_decision: {
requested_backend: 'images-api',
preferred_channel_request_mode: 'images-non-stream',
selected_channel_request_mode: 'images-non-stream'
},
selected_channel_id: 'edit-dim-channel',
upstream_host: 'edit-dim-upstream.example.test'
},
images: [{ id: 'edit-ok', filename: 'edit.png', b64_json: fakePngBase64(1024, 1024) }]
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'edit-image.mjs',
[
'--allow-billable',
'--agent',
'--dimension-check',
'--size',
'1024x1024',
'--idempotency-key',
'edit-dimension-ok-key',
imagePath,
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.deepEqual(body.images[0].dimensions, { width: 1024, height: 1024 });
assert.deepEqual(body.summary.actual_dimensions, { width: 1024, height: 1024 });
assert.equal(body.summary.channel_request_mode, 'images-non-stream');
assert.equal(body.summary.selected_channel_id, 'edit-dim-channel');
assert.equal(body.summary.upstream_host, 'edit-dim-upstream.example.test');
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('fails explicit Agent edit dimension-check mismatches with route diagnostics', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-edit-agent-dim-bad-'));
try {
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify(agentGenerateCapabilities()));
return;
}
if (request.url === '/api/agent/images/edit') {
await readRequestText(request);
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
request_id: 'edit-dimension-bad',
idempotency_key: 'edit-dimension-bad-key',
execution: {
transport: 'agent_json',
endpoint: '/api/agent/images/edit',
route_mode: 'agent',
channel_request_mode: 'images-non-stream',
channel_request_mode_fallback_applied: true,
route_decision: {
requested_backend: 'images-api',
preferred_channel_request_mode: 'images-sse',
fallback_channel_request_mode: 'images-non-stream',
selected_channel_request_mode: 'images-non-stream'
},
selected_channel_id: 'edit-dim-bad-channel',
upstream_host: 'edit-dim-bad-upstream.example.test'
},
images: [{ id: 'edit-bad', filename: 'edit.png', b64_json: fakePngBase64(1254, 1254) }]
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'edit-image.mjs',
[
'--allow-billable',
'--agent',
'--dimension-check',
'--size',
'1024x1024',
'--idempotency-key',
'edit-dimension-bad-key',
imagePath,
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.error.code, 'dimension_check_failed');
assert.equal(body.validation_failure_kind, 'generated_artifact_failed_dimension_check');
assert.deepEqual(body.error.expected_dimensions, { width: 1024, height: 1024 });
assert.deepEqual(body.error.actual_dimensions, { width: 1254, height: 1254 });
assert.equal(body.response.images[0].b64_json, undefined);
assert.equal(body.response.images[0].b64_json_length, fakePngBase64(1254, 1254).length);
assert.deepEqual(body.response.images[0].dimensions, { width: 1254, height: 1254 });
assert.equal(body.summary.dimension_check_failed, true);
assert.equal(body.summary.transport, 'agent_json');
assert.equal(body.summary.endpoint, '/api/agent/images/edit');
assert.equal(body.summary.route_mode, 'agent');
assert.equal(body.summary.channel_request_mode, 'images-non-stream');
assert.equal(body.summary.channel_request_mode_fallback_applied, true);
assert.deepEqual(body.summary.route_decision, {
requested_backend: 'images-api',
preferred_channel_request_mode: 'images-sse',
fallback_channel_request_mode: 'images-non-stream',
selected_channel_request_mode: 'images-non-stream'
});
assert.equal(body.summary.selected_channel_id, 'edit-dim-bad-channel');
assert.equal(body.summary.upstream_host, 'edit-dim-bad-upstream.example.test');
assert.match(body.summary.next_action, /确认当前编辑渠道是否支持请求尺寸/);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('enriches failed Agent edit summaries with Agent state diagnostics', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-edit-diagnostics-'));
try {
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
const requests = [];
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
}
})
);
return;
}
if (request.url === '/api/agent/images/edit') {
await readRequestText(request);
response.writeHead(500, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
error: { code: 'unexpected_error', message: 'edit failed', retryable: false }
})
);
return;
}
if (request.url === '/api/agent/diagnostics/requests?idempotency_key=diag-edit-key') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
found: true,
diagnostics: {
request: {
request_id: 'req_edit_diag',
idempotency_key: 'diag-edit-key',
status: 'failed'
},
error: {
code: 'unexpected_error',
retryable: false,
diagnostics: {
selected_channel_id: 'channel-edit',
upstream_host: 'edit-upstream.example.test',
transport_error_kind: 'socket_closed'
}
}
}
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'edit-image.mjs',
[
'--allow-billable',
'--agent',
'--idempotency-key',
'diag-edit-key',
'--size',
'3072x2048',
imagePath,
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.summary.request_id, 'req_edit_diag');
assert.equal(body.summary.selected_channel_id, 'channel-edit');
assert.equal(body.summary.upstream_host, 'edit-upstream.example.test');
assert.equal(body.summary.transport_error_kind, 'socket_closed');
assert.equal(body.summary.agent_diagnostics_checked, true);
assert.equal(body.summary.agent_diagnostics_found, true);
assert.equal(body.agent_failure_diagnostics.request_id, 'req_edit_diag');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /api/agent/capabilities',
'POST /api/agent/images/edit',
'GET /api/agent/diagnostics/requests?idempotency_key=diag-edit-key'
]
);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('rejects default WebP edit dry-runs when streaming is explicitly disabled', () => {
const result = runSkillScript('edit-image.mjs', [
'--size',
'3072x2048',
'--stream-mode',
'non_stream',
'/tmp/source.png',
'prompt'
]);
assert.equal(result.status, 2);
assert.equal(result.stdout.trim(), '');
assert.match(result.stderr, /默认 WebP 图生图输出需要页面 SSE/);
});
it('rejects explicit edit page SSE when stream-mode is non_stream before network requests', () => {
const result = runSkillScript('edit-image.mjs', [
'--allow-billable',
'--page-sse',
'--stream-mode',
'non_stream',
'/tmp/source.png',
'prompt'
]);
assert.equal(result.status, 2);
assert.equal(result.stdout.trim(), '');
assert.match(result.stderr, /stream_mode=non_stream/);
});
it('reports high-resolution edit page SSE failures as billable structured failures', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-edit-page-sse-fail-'));
try {
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
}
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
'data: {"type":"error","error":{"message":"edit stream failed"},"status":502}\n\n'
);
return;
}
if (request.url === '/api/agent/images/edit') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected Agent edit call' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'edit-image.mjs',
['--allow-billable', '--size', '3072x2048', imagePath, 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.billable, true);
assert.equal(body.error.code, 'page_sse_failed');
assert.equal(body.error.status, 502);
assert.match(body.error.message, /edit stream failed/);
assert.deepEqual(body.routing, {
transport: 'page_sse',
endpoint: '/api/images',
fallback_endpoint: '/api/agent/images/edit',
fallback_mode: 'manual_after_diagnosis'
});
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/images']
);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('reports high-resolution edit page validation failures as non-billable', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-edit-page-sse-reject-'));
try {
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
}
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(400, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'invalid edit request' }));
return;
}
if (request.url === '/api/agent/images/edit') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected Agent edit call' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'edit-image.mjs',
['--allow-billable', '--size', '3072x2048', imagePath, 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.billable, false);
assert.equal(body.summary.billable, false);
assert.equal(body.error.code, 'page_sse_request_rejected');
assert.equal(body.error.status, 400);
assert.match(body.error.message, /invalid edit request/);
assert.equal(body.routing.fallback_endpoint, '/api/agent/images/edit');
assert.equal(body.routing.fallback_mode, 'fix_request_before_retry');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/images']
);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('sends upstream streaming fields in billable edit multipart requests', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-edit-'));
try {
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
const requests = [];
let editRequestBody = '';
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/images/edit') {
editRequestBody = await readRequestText(request);
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ images: [{ id: 'edit-image', filename: 'edit.png' }] }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'edit-image.mjs',
[
'--allow-billable',
'--agent',
'--stream-mode',
'stream',
'--streaming-strategy',
'responses-sse',
'--partial-images',
'3',
'--force-request',
imagePath,
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/agent/images/edit']
);
assert.match(editRequestBody, /name="stream_mode"\r?\n\r?\nstream/);
assert.match(editRequestBody, /name="streaming_strategy"\r?\n\r?\nresponses-sse/);
assert.match(editRequestBody, /name="partial_images"\r?\n\r?\n3/);
assert.match(editRequestBody, /name="force_request"\r?\n\r?\ntrue/);
assert.match(editRequestBody, /name="image_0"; filename="source\.png"/);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('rejects invalid generate upstream streaming options before dry-run output', () => {
const invalidPartialImages = runSkillScript('generate-image.mjs', ['--partial-images', '5', 'prompt']);
assert.equal(invalidPartialImages.status, 2);
assert.match(invalidPartialImages.stderr, /--partial-images 必须是 0 到 4 的整数/);
assert.equal(invalidPartialImages.stdout.trim(), '');
const invalidBackend = runSkillScript('generate-image.mjs', ['--image-backend', 'unknown-backend', 'prompt']);
assert.equal(invalidBackend.status, 2);
assert.match(invalidBackend.stderr, /--image-backend 必须是/);
assert.equal(invalidBackend.stdout.trim(), '');
const invalidStrategy = runSkillScript('generate-image.mjs', [
'--streaming-strategy',
'unknown-strategy',
'prompt'
]);
assert.equal(invalidStrategy.status, 2);
assert.match(invalidStrategy.stderr, /--streaming-strategy 必须是/);
assert.equal(invalidStrategy.stdout.trim(), '');
const invalidResponseMode = runSkillScript('generate-image.mjs', ['--response-mode', 'url', 'prompt']);
assert.equal(invalidResponseMode.status, 2);
assert.match(invalidResponseMode.stderr, /--response-mode 必须是 path、base64 或 both/);
assert.equal(invalidResponseMode.stdout.trim(), '');
const invalidThinking = runSkillScript('generate-image.mjs', [
'--image-backend',
'responses',
'--responses-model',
'gpt-5.4-mini',
'--thinking',
'unknown',
'prompt'
]);
assert.equal(invalidThinking.status, 2);
assert.match(invalidThinking.stderr, /--thinking 必须是/);
assert.equal(invalidThinking.stdout.trim(), '');
const invalidPromptOptimization = runSkillScript('generate-image.mjs', [
'--image-backend',
'responses',
'--responses-model',
'gpt-5.4-mini',
'--prompt-optimization',
'maybe',
'prompt'
]);
assert.equal(invalidPromptOptimization.status, 2);
assert.match(invalidPromptOptimization.stderr, /--prompt-optimization 必须是 true 或 false/);
assert.equal(invalidPromptOptimization.stdout.trim(), '');
const emptyResponsesModel = runSkillScript('generate-image.mjs', [
'--image-backend',
'responses',
'--responses-model',
' ',
'prompt'
]);
assert.equal(emptyResponsesModel.status, 2);
assert.match(emptyResponsesModel.stderr, /--responses-model 必须是非空字符串/);
assert.equal(emptyResponsesModel.stdout.trim(), '');
const responsesModelWithoutBackend = runSkillScript('generate-image.mjs', [
'--responses-model',
'gpt-5.4-mini',
'prompt'
]);
assert.equal(responsesModelWithoutBackend.status, 2);
assert.match(responsesModelWithoutBackend.stderr, /--responses-model 必须同时设置 --image-backend/);
assert.equal(responsesModelWithoutBackend.stdout.trim(), '');
const responsesModelImagesBackend = runSkillScript('generate-image.mjs', [
'--image-backend',
'images-api',
'--responses-model',
'gpt-5.4-mini',
'prompt'
]);
assert.equal(responsesModelImagesBackend.status, 2);
assert.match(responsesModelImagesBackend.stderr, /--responses-model 仅适用于 --image-backend/);
assert.equal(responsesModelImagesBackend.stdout.trim(), '');
const disabledAutoPageSse = runSkillScript('generate-image.mjs', [
'--size',
'3072x2048',
'--streaming-strategy',
'off',
'prompt'
]);
assert.equal(disabledAutoPageSse.status, 0);
const disabledAutoPageSseBody = JSON.parse(disabledAutoPageSse.stdout);
assert.equal(disabledAutoPageSseBody.endpoint, 'http://localhost:4783/api/agent/image-requests');
assert.equal(disabledAutoPageSseBody.routing_guidance.recommended_endpoint, '/api/agent/image-requests');
assert.equal(disabledAutoPageSseBody.routing_guidance.transport, 'server_orchestrated');
assert.equal(disabledAutoPageSse.stderr.trim(), '');
const disabledPageSse = runSkillScript('generate-image.mjs', [
'--page-sse',
'--streaming-strategy',
'off',
'prompt'
]);
assert.equal(disabledPageSse.status, 2);
assert.match(disabledPageSse.stderr, /streaming_strategy=off/);
assert.doesNotMatch(disabledPageSse.stderr, /ModuleJob|at buildGenerateRoutingGuidance/);
assert.equal(disabledPageSse.stdout.trim(), '');
});
it('shows edit help without validating unrelated env values', () => {
const result = runSkillScript('edit-image.mjs', ['--help'], {
GPT_IMAGE_AGENT_MAX_ATTEMPTS: 'abc'
});
assert.equal(result.status, 0);
assert.match(result.stderr, /用法:edit-image\.mjs/);
assert.equal(result.stdout.trim(), '');
});
it('shows skill script help without validating service URL env values', () => {
const env = { GPT_IMAGE_PLAYGROUND_URL: 'https://user:secret@example.test' };
const generateHelp = runSkillScript('generate-image.mjs', ['--help'], env);
const editHelp = runSkillScript('edit-image.mjs', ['--help'], env);
assert.equal(generateHelp.status, 0);
assert.match(generateHelp.stderr, /用法:generate-image\.mjs/);
assert.equal(generateHelp.stdout.trim(), '');
assert.equal(editHelp.status, 0);
assert.match(editHelp.stderr, /用法:edit-image\.mjs/);
assert.equal(editHelp.stdout.trim(), '');
});
it('keeps the skill package free of machine-specific and shell-specific paths', () => {
const forbiddenPatterns = [
/\/Users\/[^`\\s)]+/,
/\/Volumes\/[^`\\s)]+/,
/\/home\/[^`\\s)]+/,
/C:\\\\Users\\\\[^`\\s)]+/i,
/\.\.\/\.\.\/\.\.\/src\//,
/\.\.\/\.\.\/\.\.\/\.\.\/src\//,
/```bash/,
/(^|\n)[A-Z_][A-Z0-9_]*=.*\s+node\s/,
/\\\r?\n\s+--/
];
const matches = [];
for (const filePath of listTextFiles(skillRoot)) {
const content = readFileSync(filePath, 'utf8');
for (const pattern of forbiddenPatterns) {
if (pattern.test(content)) {
matches.push(filePath);
break;
}
}
}
assert.deepEqual(matches, []);
});
it('keeps skill frontmatter valid without requiring Python YAML tooling', () => {
const skillText = readFileSync(join(skillRoot, 'SKILL.md'), 'utf8');
const match = skillText.match(/^---\r?\n([\s\S]*?)\r?\n---/);
assert.ok(match, 'SKILL.md must start with YAML frontmatter');
const frontmatter = Object.fromEntries(
match[1]
.split(/\r?\n/)
.filter(Boolean)
.map((line) => {
const separator = line.indexOf(':');
assert.ok(separator > 0, `invalid frontmatter line: ${line}`);
return [line.slice(0, separator).trim(), line.slice(separator + 1).trim()];
})
);
assert.deepEqual(Object.keys(frontmatter).sort(), ['description', 'name']);
assert.match(frontmatter.name, /^[a-z0-9-]+$/);
assert.ok(frontmatter.description.length > 0);
assert.ok(frontmatter.description.length <= 1024);
assert.doesNotMatch(frontmatter.description, /[<>]/);
});
it('tells agents to use bundled scripts instead of ad hoc API callers', () => {
const skillText = readFileSync(join(skillRoot, 'SKILL.md'), 'utf8');
const openAiYaml = readFileSync(join(skillRoot, 'agents/openai.yaml'), 'utf8');
const apiReference = readFileSync(join(skillRoot, 'references/api.md'), 'utf8');
assert.match(skillText, /必须优先运行本 Skill 内置 scripts\/generate-image\.mjs/);
assert.match(skillText, /scripts\/channel-capability-matrix\.mjs/);
assert.match(skillText, /不要临时编写 Node\/Python\/shell 脚本、curl 命令或手写 fetch\/FormData/);
assert.match(openAiYaml, /先选择并运行内置脚本/);
assert.match(openAiYaml, /不要临时编写 API 调用脚本/);
assert.match(apiReference, /先使用这些内置脚本/);
assert.match(apiReference, /scripts\/channel-capability-matrix\.mjs/);
assert.match(apiReference, /不要临时编写 Node\/Python\/shell 脚本、curl 命令或手写 fetch\/FormData/);
});
it('documents backend-specific partial image limits in dedicated Agent docs', () => {
const skillText = readFileSync(join(skillRoot, 'SKILL.md'), 'utf8');
const apiReference = readFileSync(join(skillRoot, 'references/api.md'), 'utf8');
assert.match(skillText, /limits\.partial_images_by_backend\[image_backend\]/);
assert.match(
skillText,
/Agent edit 不接受 `image_backend`,其内部上游流式字段按默认 Images API\/profile 范围校验/
);
assert.match(skillText, /Responses image_generation edit 属于页面 SSE 路径/);
assert.match(
skillText,
/Docker compose 本身不设置这两个默认值,未配置 `\.env\.local` 时仍是 `images-api` 和 `auto`/
);
assert.match(skillText, /脚本参数仍写作 `--streaming-strategy responses-sse`/);
assert.match(skillText, /batch JSONL 字段是 `streaming_strategy`/);
assert.match(skillText, /`image_streaming_strategy` 是页面 form-data 字段名,不是 batch JSONL 字段/);
assert.match(skillText, /`capabilities` 里声明的 `page_sse_supported=true`/);
assert.match(skillText, /`selected_channel_id`、`upstream_host` 为空/);
assert.match(skillText, /支持位置参数 `<image-path> <prompt>`,也支持 `--image <path> <prompt>` 别名/);
assert.match(skillText, /`--image-backend responses-image-generation` 只用于页面 SSE edit/);
assert.match(skillText, /不要把 Matsca `limits\.partial_images=0\.\.4` 误套到 `responses-image-generation`/);
assert.match(apiReference, /limits\.partial_images_by_backend\[image_backend\]/);
assert.match(apiReference, /只代表“声明支持”,不代表当前渠道每次实测都能成功/);
assert.match(apiReference, /如果 `selected_channel_id`、`upstream_host` 为空/);
assert.match(apiReference, /Agent edit 不接收 `image_backend`、`output_format` 或 `output_compression`/);
assert.match(
apiReference,
/强制 Agent edit 时由服务端固定使用 WebP 输出契约,`partial_images` 按默认 Images API\/profile 范围校验/
);
assert.match(
apiReference,
/图片路径可以用位置参数 `<image-path> <prompt>`,也可以用 `--image <path> <prompt>`/
);
assert.match(
apiReference,
/Agent edit 不接受 `image_backend`;需要 Responses backend edit 字段时必须使用页面端 `\/api\/images` form-data SSE 路径/
);
assert.match(apiReference, /Responses image_generation edit 必须走页面 SSE/);
assert.match(apiReference, /JSONL 字段名必须使用 `streaming_strategy`/);
assert.match(apiReference, /`image_streaming_strategy` 是页面 form-data 字段名,不是 batch JSONL 字段/);
assert.match(apiReference, /Responses image_generation 后端仅在页面 SSE `\/api\/images` 中支持编辑功能/);
assert.doesNotMatch(apiReference, /Responses image_generation 后端当前只支持 generate/);
assert.match(apiReference, /选择 `responses-image-generation` 或兼容别名 `responses` 时必须优先使用该字段/);
});
it('documents batch capacity and dimension guardrails for real Agent runs', () => {
const readmeText = readFileSync(join(repoRoot, 'README.md'), 'utf8');
const skillText = readFileSync(join(skillRoot, 'SKILL.md'), 'utf8');
const apiReference = readFileSync(join(skillRoot, 'references/api.md'), 'utf8');
assert.match(readmeText, /npm run first-run/);
assert.match(readmeText, /npm run first-run -- --json/);
assert.match(readmeText, /\.env\.agent\.local\.example/);
assert.match(readmeText, /--base-url http:\/\/localhost:4783/);
assert.match(skillText, /npm run first-run/);
assert.match(skillText, /-- --json/);
assert.match(skillText, /--base-url <url>/);
assert.match(skillText, /\.env\.agent\.local\.example/);
assert.match(skillText, /page_sse_real_smoke_status/);
assert.match(skillText, /responses_image_backend_real_smoke_status/);
assert.match(skillText, /summary\.page_sse_real_smoke/);
assert.match(skillText, /兼容聚合状态/);
assert.match(skillText, /summary\.responses_page_sse_generate_smoke/);
assert.match(skillText, /summary\.responses_agent_generate_smoke/);
assert.match(skillText, /responses_agent_generate_1k/);
assert.match(skillText, /request_mode_controls/);
assert.match(skillText, /summary\.real_smoke_checks/);
assert.match(apiReference, /npm run first-run/);
assert.match(apiReference, /npm run first-run -- --json/);
assert.match(apiReference, /--base-url <url>/);
assert.match(apiReference, /\.env\.agent\.local\.example/);
assert.match(apiReference, /page_sse_real_smoke_status/);
assert.match(apiReference, /responses_image_backend_real_smoke_status/);
assert.match(apiReference, /summary\.page_sse_real_smoke/);
assert.match(apiReference, /兼容聚合状态/);
assert.match(apiReference, /summary\.responses_page_sse_generate_smoke/);
assert.match(apiReference, /summary\.responses_agent_generate_smoke/);
assert.match(apiReference, /responses_agent_generate_1k/);
assert.match(apiReference, /request_mode_controls/);
assert.match(apiReference, /summary\.real_smoke_checks/);
assert.match(skillText, /公开分享可直接打开 `direct_content_urls`,设置访问码时优先给用户 `share_urls`/);
assert.match(apiReference, /公开分享可直接打开 `direct_content_urls`,设置访问码时优先给用户 `share_urls`/);
assert.match(apiReference, /顶层 `shares`、`summary\.share_urls` 和 `summary\.direct_content_urls` 输出结果/);
assert.match(skillText, /不要手动并行启动多个单张脚本/);
assert.match(skillText, /capacity_feedback/);
assert.match(skillText, /channelQueue\.capacityPerCredential/);
assert.match(skillText, /输出格式固定为 Agent WebP 契约/);
assert.match(skillText, /Agent edit 只是对照路径,不保证与页面 SSE 的像素尺寸完全一致/);
assert.match(skillText, /复杂 UI、长 prompt、高质量图生图遇到 5 分钟级超时/);
assert.match(skillText, /Codex 会话日志会持久保存命令输出/);
assert.match(skillText, /npm run env:summary/);
assert.match(skillText, /verification_scope\.mode=local_planning_only/);
assert.match(skillText, /--check-remote/);
assert.match(skillText, /remote_contract_and_local_planning/);
assert.match(skillText, /manifest_written=false/);
assert.match(skillText, /Hugging Face Space Secrets 只能写入和列出名称/);
assert.match(apiReference, /不要手动并行启动多个单张脚本来绕过 `capacity_feedback`/);
assert.match(apiReference, /streamingBatch\.recommendedConcurrency/);
assert.match(apiReference, /channelQueue\.capacityPerCredential/);
assert.match(apiReference, /尺寸敏感任务必须使用生成\/编辑\/批量 `--dimension-check` 或下载后校验/);
assert.match(apiReference, /编辑 `--dimension-check` 会读取响应 `b64_json` 或同 origin `content_url`/);
assert.match(apiReference, /channel_capacity_queue_aborted/);
assert.match(apiReference, /npm run env:summary/);
assert.match(apiReference, /verification_scope\.mode=local_planning_only/);
assert.match(apiReference, /--check-remote/);
assert.match(apiReference, /remote_contract_and_local_planning/);
assert.match(apiReference, /manifest_written=false/);
assert.match(apiReference, /Hugging Face Space Secrets 只能写入和列出名称/);
assert.match(apiReference, /这个失败表示上游已生成但本地验收未通过/);
assert.match(apiReference, /`validation_failure_count>0` 而 `request_failure_count=0`/);
});
it('keeps WebUI page APIs out of the Agent OpenAPI contract', () => {
const readmeText = readFileSync(join(repoRoot, 'README.md'), 'utf8');
const skillText = readFileSync(join(skillRoot, 'SKILL.md'), 'utf8');
const apiReference = readFileSync(join(skillRoot, 'references/api.md'), 'utf8');
const openApiSource = readFileSync(join(repoRoot, 'src/lib/agent-openapi.ts'), 'utf8');
const agentEndpointValues = Object.values(AGENT_ENDPOINTS);
const pageApiBoundaries = [
'/api/images',
'/api/runtime-capabilities',
'/api/feedback',
'/api/shares',
'/api/logs',
'/api/image-delete'
];
const agentReadableDiagnostics = [
'/api/agent/page-requests/feedback',
'/api/agent/page-requests/{id}/feedback',
'/api/agent/diagnostics/page-requests',
'/api/agent/diagnostics/page-requests/{id}',
'/api/agent/diagnostics/channel-health'
];
const localWorkbenchFeatures = ['灵感相册', '历史复用'];
for (const endpoint of pageApiBoundaries) {
assert.match(skillText, new RegExp(escapeRegExp(endpoint)));
assert.match(apiReference, new RegExp(escapeRegExp(endpoint)));
assert.equal(agentEndpointValues.includes(endpoint), false);
}
for (const feature of localWorkbenchFeatures) {
assert.match(readmeText, new RegExp(escapeRegExp(feature)));
assert.match(skillText, new RegExp(escapeRegExp(feature)));
assert.match(apiReference, new RegExp(escapeRegExp(feature)));
assert.match(apiReference, new RegExp(`${escapeRegExp(feature)}[\\s\\S]*不作为机器 API 契约承诺`));
}
for (const endpoint of agentReadableDiagnostics) {
assert.match(skillText, new RegExp(escapeRegExp(endpoint)));
assert.match(apiReference, new RegExp(escapeRegExp(endpoint)));
assert.equal(agentEndpointValues.includes(endpoint), true);
}
assert.match(readmeText, /边界矩阵/);
assert.match(apiReference, /### 边界矩阵/);
assert.match(openApiSource, /AGENT_ENDPOINTS\.page_request_feedback_batch/);
assert.match(openApiSource, /AGENT_ENDPOINTS\.page_request_feedback/);
assert.match(openApiSource, /AGENT_ENDPOINTS\.page_request_diagnostics_batch/);
assert.match(openApiSource, /AGENT_ENDPOINTS\.page_request_diagnostics/);
assert.match(openApiSource, /高分辨率 edit 默认优先使用页面端 \/api\/images form-data SSE/);
});
it('requires new probe and diagnostics work to keep API contracts ahead of Skill wrappers', () => {
const readmeText = readFileSync(join(repoRoot, 'README.md'), 'utf8');
const skillText = readFileSync(join(skillRoot, 'SKILL.md'), 'utf8');
const apiReference = readFileSync(join(skillRoot, 'references/api.md'), 'utf8');
assert.match(
readmeText,
/新增 probe、diagnostics 或路由可观测能力时,先落 API \/ capabilities \/ OpenAPI 契约/
);
assert.match(readmeText, /Skill 脚本做薄封装/);
assert.match(skillText, /新增 probe、diagnostics、路由健康或请求旅程能力时/);
assert.match(skillText, /GET \/api\/agent\/capabilities/);
assert.match(skillText, /GET \/api\/agent\/openapi\.json/);
assert.match(skillText, /\/api\/agent\/diagnostics\/\*/);
assert.match(skillText, /Skill 脚本只做薄封装/);
assert.match(skillText, /不能复制页面 API、运行态 API 和 Agent API 的边界判断/);
assert.match(apiReference, /新增 probe、diagnostics 或健康摘要时/);
assert.match(apiReference, /capabilities、OpenAPI 或明确的 Agent 只读端点/);
assert.match(apiReference, /不要让脚本自己拼 page API、runtime API 和 Agent API 的边界逻辑/);
});
it('runs from a copied standalone skill directory outside the repository', () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-playground-agent-'));
const copiedSkillRoot = join(tempRoot, 'gpt-image-playground-agent');
try {
cpSync(skillRoot, copiedSkillRoot, { recursive: true });
writeFileSync(
join(tempRoot, '.env.agent.local'),
'GPT_IMAGE_PLAYGROUND_URL=https://parent-space.example.test'
);
const standaloneRun = spawnSync(
process.execPath,
[join(copiedSkillRoot, 'scripts/generate-image.mjs'), 'prompt'],
{
cwd: join(copiedSkillRoot, 'scripts'),
encoding: 'utf8',
env: {
...buildIsolatedSkillScriptEnv({ loadPrivateAgentEnv: true }),
GPT_IMAGE_AGENT_LOAD_ENV_FILE: '1'
}
}
);
assert.equal(standaloneRun.status, 0);
const standaloneBody = JSON.parse(standaloneRun.stdout);
assert.equal(standaloneBody.verification_scope.service_base_url, 'http://localhost:4783');
const result = spawnSync(
process.execPath,
[join(copiedSkillRoot, 'scripts/generate-image.mjs'), '--help'],
{
cwd: tmpdir(),
encoding: 'utf8',
env: {
...process.env,
GPT_IMAGE_PLAYGROUND_URL: 'not a url'
}
}
);
assert.equal(result.status, 0);
assert.match(result.stderr, /用法:generate-image\.mjs/);
assert.equal(result.stdout.trim(), '');
assert.doesNotMatch(`${result.stdout}\n${result.stderr}`, /ERR_MODULE_NOT_FOUND|src\/lib/);
const convertHelp = spawnSync(
process.execPath,
[join(copiedSkillRoot, 'scripts/convert-image-format.mjs'), '--help'],
{
cwd: tmpdir(),
encoding: 'utf8',
env: {
...process.env,
NODE_PATH: ''
}
}
);
assert.equal(convertHelp.status, 0);
assert.match(convertHelp.stderr, /用法:convert-image-format\.mjs/);
assert.equal(convertHelp.stdout.trim(), '');
assert.doesNotMatch(`${convertHelp.stdout}\n${convertHelp.stderr}`, /ERR_MODULE_NOT_FOUND|sharp/);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('prints batch dry-run JSONL plans without contacting the service', () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(
inputPath,
[
JSON.stringify({
id: 'first item',
prompt: 'first prompt',
size: '1024x1024',
output_format: 'jpg',
output_compression: '80'
}),
JSON.stringify({
mode: 'edit',
id: 'edit item',
prompt: 'edit prompt',
image_paths: ['/tmp/source-a.png', '/tmp/source-b.png'],
mask_path: '/tmp/mask.png'
})
].join('\n')
);
const result = runSkillScript('batch-images.mjs', ['--input', inputPath, '--ordered-prefix', 'demo']);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.dry_run, true);
assert.equal(body.billable, false);
assert.equal(body.verification_scope.mode, 'local_planning_only');
assert.equal(body.verification_scope.remote_capabilities_verified, false);
assert.equal(body.verification_scope.runtime_capacity_verified, false);
assert.equal(body.manifest_written, false);
assert.equal(body.manifest_write_reason, 'dry_run');
assert.equal(body.total, 2);
assert.equal(body.concurrency, 1);
assert.equal(body.guardrails.ordered_prefix, 'demo');
assert.equal(body.guardrails.repeat_ordered_prefix_on_real_run, true);
assert.equal(body.guardrails.dimension_check_recommended, true);
assert.match(body.guardrails.dimension_check_reason, /--dimension-check/);
assert.equal(body.tasks[0].endpoint, '/api/agent/image-requests');
assert.equal(body.tasks[0].idempotency_key, 'demo-0001-first-item');
assert.equal(body.tasks[0].request.model, 'gpt-image-2');
assert.equal(body.tasks[0].request.output_format, 'jpeg');
assert.equal(body.tasks[0].request.output_compression, 80);
assert.equal(body.tasks[1].endpoint, '/api/images');
assert.equal(body.tasks[1].routing.transport, 'page_sse');
assert.equal(body.tasks[1].idempotency_key, 'demo-0002-edit-item');
assert.deepEqual(body.tasks[1].request.image_fields, ['image_0', 'image_1']);
assert.equal(body.tasks[1].request.mask, 'provided');
assert.equal(body.tasks[1].request.output_format, 'webp');
assert.equal(body.tasks[1].request.output_compression, 100);
assert.equal('image_path' in body.tasks[1].request, false);
assert.equal('image_paths' in body.tasks[1].request, false);
assert.equal('mask_path' in body.tasks[1].request, false);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('prints stdin batch dry-run plans without inventing a /dev/stdin manifest path', () => {
const input = `${JSON.stringify({
id: 'stdin-task',
prompt: 'stdin prompt',
size: '1024x1024'
})}\n`;
const result = runSkillScript('batch-images.mjs', ['--input', '-'], {}, { input });
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.input, '/dev/stdin');
assert.equal(body.manifest, null);
assert.equal(body.manifest_written, false);
assert.equal(body.total, 1);
assert.equal(body.tasks[0].id, 'stdin-task');
});
it('records enriched Agent diagnostics for failed batch tasks and manifests', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-diagnostics-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
const manifestPath = join(tempRoot, 'manifest.jsonl');
writeFileSync(
inputPath,
JSON.stringify({ id: 'diag-task', prompt: 'prompt', idempotency_key: 'batch-diag-key' })
);
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/image-requests') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
error: { code: 'unexpected_error', message: 'fetch failed', retryable: false }
})
);
return;
}
if (request.url === '/api/agent/diagnostics/requests?idempotency_key=batch-diag-key') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
found: true,
diagnostics: {
request: {
request_id: 'req_batch_diag',
idempotency_key: 'batch-diag-key',
status: 'failed'
},
error: {
code: 'unexpected_error',
retryable: false,
diagnostics: {
selected_channel_id: 'channel-b',
upstream_host: 'batch-upstream.example.test'
}
}
}
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath, '--manifest', manifestPath, '--max-attempts', '1'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.results[0].summary.request_id, 'req_batch_diag');
assert.equal(body.results[0].summary.selected_channel_id, 'channel-b');
assert.equal(body.results[0].summary.upstream_host, 'batch-upstream.example.test');
assert.equal(body.results[0].summary.agent_diagnostics_checked, true);
assert.equal(body.results[0].agent_failure_diagnostics.request_id, 'req_batch_diag');
const manifestLines = readFileSync(manifestPath, 'utf8').trim().split(/\r?\n/).map(JSON.parse);
assert.equal(manifestLines[0].summary.request_id, 'req_batch_diag');
assert.equal(manifestLines[0].agent_failure_diagnostics.request_id, 'req_batch_diag');
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('rejects invalid batch concurrency before dry-run output', () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-concurrency-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(inputPath, JSON.stringify({ id: 'first', prompt: 'prompt' }));
const result = runSkillScript('batch-images.mjs', ['--input', inputPath, '--concurrency', '0']);
assert.equal(result.status, 2);
assert.match(result.stderr, /--concurrency 必须是正整数/);
assert.equal(result.stdout.trim(), '');
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('rejects batch concurrency with strict consecutive failure stopping', () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-concurrency-stop-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(inputPath, JSON.stringify({ id: 'first', prompt: 'prompt' }));
const result = runSkillScript('batch-images.mjs', [
'--input',
inputPath,
'--concurrency',
'2',
'--max-consecutive-failures',
'1'
]);
assert.equal(result.status, 2);
assert.match(result.stderr, /不能同时使用 --max-consecutive-failures/);
assert.equal(result.stdout.trim(), '');
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('preserves explicit zero partial image requests in batch dry-run output', () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(inputPath, JSON.stringify({ id: 'zero-partial', prompt: 'prompt', partial_images: 0 }));
const result = runSkillScript('batch-images.mjs', ['--input', inputPath]);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.tasks[0].request.partial_images, 0);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('preserves explicit falsy batch task ids in dry-run plans', () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(inputPath, JSON.stringify({ id: 0, prompt: 'prompt' }));
const result = runSkillScript('batch-images.mjs', ['--input', inputPath, '--ordered-prefix', 'demo']);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.tasks[0].id, '0');
assert.equal(body.tasks[0].idempotency_key, 'demo-0001-0');
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('normalizes batch output compression before sending Agent JSON', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-compression-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(
inputPath,
JSON.stringify({
id: 'compressed',
prompt: 'prompt',
output_format: 'jpeg',
output_compression: '80'
})
);
let agentRequestBody = '';
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/image-requests') {
agentRequestBody = await readRequestText(request);
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({ images: [{ id: 'compressed-image', filename: 'compressed.jpg' }] })
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath],
{
GPT_IMAGE_PLAYGROUND_URL: baseUrl
}
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const requestBody = JSON.parse(agentRequestBody);
assert.equal(requestBody.output_format, 'jpeg');
assert.equal(requestBody.output_compression, 80);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('rejects batch Agent JSON requests outside the advertised partial image capability', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-partial-capability-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(
inputPath,
JSON.stringify({
id: 'too-many-partials',
prompt: 'prompt',
partial_images: 4
})
);
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
limits: {
partial_images: { min: 1, max: 3 }
}
})
);
return;
}
if (request.url === '/api/agent/image-requests') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'generate should not be called' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stderr.trim(), '');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities']
);
const body = JSON.parse(result.stdout);
assert.equal(body.results[0].status, 'failed');
assert.match(body.results[0].error, /partial_images 必须在当前 capabilities 允许的 1 到 3 之间/);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('rejects batch page SSE partial images outside the selected backend capability before POST', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-page-partial-capability-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(
inputPath,
JSON.stringify({
id: 'responses-zero-partial',
prompt: 'prompt',
image_backend: 'responses-image-generation',
responsesModel: 'gpt-5.4-mini',
partial_images: 0
})
);
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: { page_sse: { supported: true } },
defaults: { partial_images: 2 },
limits: {
partial_images: { min: 0, max: 4 },
partial_images_by_backend: {
'images-api': { min: 0, max: 4 },
'responses-image-generation': { min: 1, max: 3 }
}
}
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'page SSE should not be called' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stderr.trim(), '');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities']
);
const body = JSON.parse(result.stdout);
assert.equal(body.failure_summary.non_billable_count, 1);
assert.equal(body.failure_summary.billable_count, 0);
assert.equal(body.results[0].billable, false);
assert.equal(body.results[0].status, 'failed');
assert.match(body.results[0].error, /partial_images 必须在当前 capabilities 允许的 1 到 3 之间/);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('appends batch manifests and skips succeeded tasks during resume', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
const manifestPath = join(tempRoot, 'manifest.jsonl');
writeFileSync(
inputPath,
[
JSON.stringify({ id: 'first', prompt: 'first prompt', size: '1024x1024' }),
JSON.stringify({ id: 'second', prompt: 'second prompt', size: '1024x1024' })
].join('\n')
);
writeFileSync(
manifestPath,
`${JSON.stringify({ id: 'first', idempotency_key: 'batch-0001-first', status: 'succeeded' })}\n`
);
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/image-requests') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
images: [
{
id: 'image-second',
filename: 'second.png',
b64_json: fakePngBase64(2, 1)
}
]
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--resume', '--input', inputPath, '--manifest', manifestPath],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.results[0].status, 'skipped');
assert.equal(body.results[1].status, 'succeeded');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/agent/image-requests']
);
const manifestLines = readFileSync(manifestPath, 'utf8').trim().split(/\r?\n/).map(JSON.parse);
assert.equal(manifestLines.length, 3);
assert.equal(manifestLines[1].status, 'skipped');
assert.equal(manifestLines[2].status, 'succeeded');
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('runs batch tasks with the requested concurrency while preserving result order', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-concurrent-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(
inputPath,
[
JSON.stringify({ id: 'first', prompt: 'first prompt', idempotency_key: 'first-key' }),
JSON.stringify({ id: 'second', prompt: 'second prompt', idempotency_key: 'second-key' }),
JSON.stringify({ id: 'third', prompt: 'third prompt', idempotency_key: 'third-key' })
].join('\n')
);
let activeGenerateRequests = 0;
let maxActiveGenerateRequests = 0;
let enteredGenerateRequests = 0;
const requestKeys = [];
const twoGenerateRequestsEntered = createDeferred();
const releaseGenerateResponses = createDeferred();
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/image-requests') {
activeGenerateRequests += 1;
enteredGenerateRequests += 1;
maxActiveGenerateRequests = Math.max(maxActiveGenerateRequests, activeGenerateRequests);
requestKeys.push(request.headers['idempotency-key']);
if (enteredGenerateRequests === 2) {
twoGenerateRequestsEntered.resolve();
}
await releaseGenerateResponses.promise;
activeGenerateRequests -= 1;
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
images: [
{
id: request.headers['idempotency-key'],
filename: `${request.headers['idempotency-key']}.png`,
b64_json: fakePngBase64(2, 1)
}
]
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const resultPromise = runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath, '--concurrency', '2'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl },
{ timeoutMs: 10_000 }
);
let result;
try {
await waitWithTimeout(
twoGenerateRequestsEntered.promise,
2_000,
'expected two generate requests to enter before any response was released'
);
assert.equal(maxActiveGenerateRequests, 2);
releaseGenerateResponses.resolve();
result = await resultPromise;
} finally {
releaseGenerateResponses.resolve();
await resultPromise;
}
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.concurrency, 2);
assert.equal(maxActiveGenerateRequests, 2);
assert.deepEqual([...requestKeys].sort(), ['first-key', 'second-key', 'third-key']);
assert.deepEqual(
body.results.map((item) => item.id),
['first', 'second', 'third']
);
assert.deepEqual(
body.results.map((item) => item.response.images[0].filename),
['first-key.png', 'second-key.png', 'third-key.png']
);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('limits batch worker concurrency to runtime capacity feedback when requested concurrency is higher', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-capacity-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(
inputPath,
[
JSON.stringify({ id: 'first', prompt: 'first prompt', idempotency_key: 'first-key' }),
JSON.stringify({ id: 'second', prompt: 'second prompt', idempotency_key: 'second-key' })
].join('\n')
);
let activeGenerateRequests = 0;
let maxActiveGenerateRequests = 0;
const requests = [];
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/runtime-capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
streamingBatch: { recommendedConcurrency: 1 },
channelQueue: {
enabled: true,
capacityPerCredential: 1,
maxWaitMs: 420000,
maxSize: 50,
active: 0,
queued: 0
}
})
);
return;
}
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/image-requests') {
activeGenerateRequests += 1;
maxActiveGenerateRequests = Math.max(maxActiveGenerateRequests, activeGenerateRequests);
await new Promise((resolve) => setTimeout(resolve, 20));
activeGenerateRequests -= 1;
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
images: [
{
id: request.headers['idempotency-key'],
filename: `${request.headers['idempotency-key']}.png`,
b64_json: fakePngBase64(2, 1)
}
]
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath, '--concurrency', '2'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.requested_concurrency, 2);
assert.equal(body.effective_concurrency, 1);
assert.equal(body.capacity_feedback.adjusted, true);
assert.equal(body.capacity_feedback.recommended_concurrency, 1);
assert.equal(body.capacity_feedback.channel_queue.enabled, true);
assert.equal(maxActiveGenerateRequests, 1);
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /api/runtime-capabilities',
'GET /api/agent/capabilities',
'POST /api/agent/image-requests',
'POST /api/agent/image-requests'
]
);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('does not require capabilities when resume skips every batch task', () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-resume-skip-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
const manifestPath = join(tempRoot, 'manifest.jsonl');
writeFileSync(inputPath, JSON.stringify({ id: 'done', prompt: 'already done', size: '1024x1024' }));
writeFileSync(
manifestPath,
`${JSON.stringify({ id: 'done', idempotency_key: 'batch-0001-done', status: 'succeeded' })}\n`
);
const result = runSkillScript(
'batch-images.mjs',
['--allow-billable', '--resume', '--input', inputPath, '--manifest', manifestPath],
{ GPT_IMAGE_PLAYGROUND_URL: 'http://127.0.0.1:9' }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.results[0].status, 'skipped');
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('skips malformed batch manifest lines during resume', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
const manifestPath = join(tempRoot, 'manifest.jsonl');
writeFileSync(inputPath, JSON.stringify({ id: 'first', prompt: 'first prompt', size: '1024x1024' }));
writeFileSync(
manifestPath,
`${JSON.stringify({ id: 'first', idempotency_key: 'batch-0001-first', status: 'succeeded' })}\n{"truncated"`
);
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/image-requests') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected resumed request' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--resume', '--input', inputPath, '--manifest', manifestPath],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.results[0].status, 'skipped');
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('runs default WebP batch edit tasks through page SSE multipart input', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
writeFileSync(
inputPath,
JSON.stringify({
mode: 'edit',
id: 'edit-one',
prompt: 'edit prompt',
image_path: imagePath,
size: '1024x1024'
})
);
const requests = [];
let editRequestBody = '';
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: { page_sse: { supported: true, endpoint: '/api/images' } }
})
);
return;
}
if (request.url === '/api/images') {
editRequestBody = await readRequestText(request);
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"edit-one.webp","path":"/generated/edit-one.webp"}',
'',
'data: {"type":"done","images":[{"filename":"edit-one.webp","path":"/generated/edit-one.webp"}]}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath],
{
GPT_IMAGE_PLAYGROUND_URL: baseUrl
}
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.results[0].routing.transport, 'page_sse');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/images']
);
assert.match(editRequestBody, /name="prompt"\r?\n\r?\nedit prompt/);
assert.match(editRequestBody, /name="image_0"; filename="source\.png"/);
assert.match(editRequestBody, /name="output_format"\r?\n\r?\nwebp/);
assert.match(editRequestBody, /name="output_compression"\r?\n\r?\n100/);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('routes high-resolution batch edit tasks through page SSE', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-page-sse-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
writeFileSync(
inputPath,
JSON.stringify({
mode: 'edit',
id: 'edit-large',
prompt: 'edit prompt',
image_path: imagePath,
size: '3072x2048',
idempotency_key: 'batch-edit-page-sse-key'
})
);
let pageSseRequestBody = '';
const requests = [];
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
}
})
);
return;
}
if (request.url === '/api/images') {
pageSseRequestBody = await readRequestText(request);
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"batch-edit.png","path":"/generated/batch-edit.png"}',
'',
'data: {"type":"done","images":[{"filename":"batch-edit.png","path":"/generated/batch-edit.png"}]}',
'',
''
].join('\n')
);
return;
}
if (request.url === '/api/agent/images/edit') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected Agent edit call' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath],
{
GPT_IMAGE_PLAYGROUND_URL: baseUrl
}
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.results[0].routing.transport, 'page_sse');
assert.equal(body.results[0].routing.strength, 'default');
assert.equal(body.results[0].summary.ok, true);
assert.equal(body.results[0].summary.transport, 'page_sse');
assert.equal(body.results[0].summary.endpoint, '/api/images');
assert.equal(typeof body.results[0].summary.elapsed_ms, 'number');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/images']
);
assert.match(pageSseRequestBody, /name="mode"\r?\n\r?\nedit/);
assert.match(pageSseRequestBody, /name="clientRequestId"\r?\n\r?\nbatch-edit-page-sse-key/);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('routes batch generate requests with responsesModel through server orchestration', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-responses-model-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(
inputPath,
JSON.stringify({
id: 'responses-generate',
prompt: 'prompt',
image_backend: 'responses-image-generation',
streaming_strategy: 'responses-sse',
responsesModel: 'gpt-4.1-responses',
idempotency_key: 'batch-responses-model-key'
})
);
let pageSseRequestBody = '';
const requests = [];
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
orchestration: {
supported: true,
transport_selection: 'server_owned',
endpoint: '/api/agent/image-requests'
},
agent_jobs: { supported: true, mode: 'job_polling' },
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
}
})
);
return;
}
if (request.url === '/api/agent/image-requests') {
pageSseRequestBody = await readRequestText(request);
response.writeHead(202, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
job: {
id: 'batch-job-responses',
state: 'running',
result_url: '/api/agent/jobs/batch-job-responses/result',
retry_after_seconds: 1
}
})
);
return;
}
if (request.url === '/api/agent/jobs/batch-job-responses/result') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
request_id: 'batch-job-responses',
idempotency_key: 'batch-responses-model-key',
cached: false,
images: [
{
id: 'responses-artifact',
filename: 'responses.png',
content_url: '/generated/responses.png'
}
],
execution: {
transport: 'agent_job_polling',
endpoint: '/api/agent/image-requests',
route_mode: 'job',
image_backend: 'responses-image-generation',
stream_mode: 'auto',
streaming_strategy: 'auto',
channel_request_mode: 'responses-sse',
channel_request_mode_fallback_applied: false,
route_decision: {
requested_backend: 'responses-image-generation',
preferred_channel_request_mode: 'responses-sse',
selected_channel_request_mode: 'responses-sse',
fallback_applied: false,
selected_channel_id: 'responses-channel',
upstream_host: 'upstream.example.test'
}
},
timing: { server_elapsed_ms: 1234 }
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath],
{
GPT_IMAGE_PLAYGROUND_URL: baseUrl
}
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.deepEqual(body.results[0].routing, {
strength: 'default',
transport: 'server_orchestrated',
endpoint: '/api/agent/image-requests',
route_mode: 'job',
result_mode: 'job_polling',
reason: 'Batch generate tasks submit intent to the server orchestration endpoint; the service owns internal route selection.'
});
assert.equal(body.results[0].summary.transport, 'agent_job_polling');
assert.equal(body.results[0].summary.endpoint, '/api/agent/image-requests');
assert.equal(body.results[0].summary.route_mode, 'job');
assert.equal(body.results[0].summary.channel_request_mode, 'responses-sse');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /api/agent/capabilities',
'POST /api/agent/image-requests',
'GET /api/agent/jobs/batch-job-responses/result'
]
);
const requestBody = JSON.parse(pageSseRequestBody);
assert.equal(requestBody.image_backend, 'responses-image-generation');
assert.equal(requestBody.responsesModel, 'gpt-4.1-responses');
assert.equal(requestBody.streaming_strategy, 'responses-sse');
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('passes GPT2Image-compatible edit options to batch page SSE form-data', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-edit-advanced-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
writeFileSync(
inputPath,
JSON.stringify({
mode: 'edit',
id: 'edit-advanced',
prompt: 'edit prompt',
image_path: imagePath,
size: '1024x1024',
output_format: 'jpeg',
output_compression: 85,
moderation: 'auto',
image_backend: 'responses',
responsesModel: 'gpt-5.4-mini',
thinking: 'medium',
promptOptimization: false,
force_web: true,
idempotency_key: 'batch-edit-advanced-key'
})
);
let pageSseRequestBody = '';
const requests = [];
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
}
})
);
return;
}
if (request.url === '/api/images') {
pageSseRequestBody = await readRequestText(request);
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"edit.jpg","path":"/generated/edit.jpg"}',
'',
'data: {"type":"done","images":[{"filename":"edit.jpg","path":"/generated/edit.jpg"}]}',
'',
''
].join('\n')
);
return;
}
if (request.url === '/api/agent/images/edit') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected Agent edit call' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath],
{
GPT_IMAGE_PLAYGROUND_URL: baseUrl
}
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.results[0].routing.transport, 'page_sse');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/images']
);
assert.match(pageSseRequestBody, /name="mode"\r?\n\r?\nedit/);
assert.match(pageSseRequestBody, /name="output_format"\r?\n\r?\njpeg/);
assert.match(pageSseRequestBody, /name="output_compression"\r?\n\r?\n85/);
assert.match(pageSseRequestBody, /name="moderation"\r?\n\r?\nauto/);
assert.match(pageSseRequestBody, /name="image_backend"\r?\n\r?\nresponses-image-generation/);
assert.match(pageSseRequestBody, /name="responsesModel"\r?\n\r?\ngpt-5\.4-mini/);
assert.match(pageSseRequestBody, /name="thinking"\r?\n\r?\nmedium/);
assert.match(pageSseRequestBody, /name="promptOptimization"\r?\n\r?\nfalse/);
assert.match(pageSseRequestBody, /name="force_web"\r?\n\r?\ntrue/);
assert.match(pageSseRequestBody, /name="image_0"/);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('keeps batch responsesModel routing on server orchestration when page SSE capability is unavailable', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-responses-model-no-sse-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(
inputPath,
JSON.stringify({
id: 'responses-no-page-sse',
prompt: 'prompt',
image_backend: 'responses-image-generation',
responsesModel: 'gpt-4.1-responses'
})
);
const requests = [];
await withServer(
async (request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
orchestration: {
supported: true,
transport_selection: 'server_owned',
endpoint: '/api/agent/image-requests'
},
agent_jobs: { supported: true, mode: 'job_polling' }
})
);
return;
}
if (request.url === '/api/agent/image-requests') {
response.writeHead(202, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
job: {
id: 'batch-no-page-sse-job',
state: 'running',
result_url: '/api/agent/jobs/batch-no-page-sse-job/result',
retry_after_seconds: 1
}
})
);
return;
}
if (request.url === '/api/agent/jobs/batch-no-page-sse-job/result') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
request_id: 'batch-no-page-sse-job',
idempotency_key: 'responses-no-page-sse',
cached: false,
images: [
{
id: 'batch-no-page-sse-artifact',
filename: 'responses.png',
content_url: '/generated/responses.png'
}
],
execution: {
transport: 'agent_job_polling',
endpoint: '/api/agent/image-requests',
route_mode: 'job',
image_backend: 'responses-image-generation',
stream_mode: 'auto',
streaming_strategy: 'auto',
channel_request_mode: 'images-non-stream',
channel_request_mode_fallback_applied: false,
route_decision: {
requested_backend: 'responses-image-generation',
preferred_channel_request_mode: 'images-non-stream',
selected_channel_request_mode: 'images-non-stream',
fallback_applied: false,
selected_channel_id: 'images-channel',
upstream_host: 'upstream.example.test'
}
},
timing: { server_elapsed_ms: 987 }
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath],
{
GPT_IMAGE_PLAYGROUND_URL: baseUrl
}
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.results[0].ok, true);
assert.deepEqual(body.results[0].routing, {
strength: 'default',
transport: 'server_orchestrated',
endpoint: '/api/agent/image-requests',
route_mode: 'job',
result_mode: 'job_polling',
reason: 'Batch generate tasks submit intent to the server orchestration endpoint; the service owns internal route selection.'
});
assert.equal(body.results[0].summary.transport, 'agent_job_polling');
assert.equal(body.results[0].summary.channel_request_mode, 'images-non-stream');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /api/agent/capabilities',
'POST /api/agent/image-requests',
'GET /api/agent/jobs/batch-no-page-sse-job/result'
]
);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('does not route batch generate tasks through page SSE when streaming is explicitly disabled', () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-disabled-page-sse-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(
inputPath,
JSON.stringify({
id: 'large-generate-non-stream',
prompt: 'prompt',
size: '3072x2048',
stream_mode: 'non_stream'
})
);
const result = runSkillScript('batch-images.mjs', ['--input', inputPath]);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.tasks[0].routing.transport, 'server_orchestrated');
assert.equal(body.tasks[0].routing.endpoint, '/api/agent/image-requests');
assert.equal(body.tasks[0].routing.result_mode, 'job_polling');
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('rejects explicit batch page SSE when streaming is explicitly disabled', () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-disabled-explicit-page-sse-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(
inputPath,
JSON.stringify({
id: 'large-generate-forced-page-sse',
prompt: 'prompt',
size: '3072x2048',
page_sse: true,
stream_mode: 'non_stream'
})
);
const result = runSkillScript('batch-images.mjs', ['--input', inputPath]);
assert.equal(result.status, 2);
assert.equal(result.stdout.trim(), '');
assert.match(result.stderr, /large-generate-forced-page-sse/);
assert.match(result.stderr, /stream_mode=non_stream/);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('records structured page SSE failures in batch output and manifest', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-page-sse-fail-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
const manifestPath = join(tempRoot, 'manifest.jsonl');
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
writeFileSync(
inputPath,
JSON.stringify({
mode: 'edit',
id: 'edit-large-fail',
prompt: 'edit prompt',
image_path: imagePath,
size: '3072x2048',
idempotency_key: 'batch-edit-page-sse-fail-key'
})
);
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
}
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(400, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'invalid edit request' }));
return;
}
if (request.url === '/api/agent/images/edit') {
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected Agent edit call' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath, '--manifest', manifestPath],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.ok, false);
assert.equal(body.failed, 1);
assert.equal(body.results[0].billable, false);
assert.equal(body.results[0].error.code, 'page_sse_request_rejected');
assert.equal(body.results[0].error.status, 400);
assert.equal(body.results[0].routing.fallback_endpoint, '/api/agent/images/edit');
assert.equal(body.results[0].routing.fallback_mode, 'fix_request_before_retry');
assert.match(body.results[0].next_step, /请求参数或鉴权/);
assert.equal(body.results[0].summary.ok, false);
assert.equal(body.results[0].summary.billable, false);
assert.equal(body.results[0].summary.transport, 'page_sse');
assert.equal(body.results[0].summary.endpoint, '/api/images');
const manifestLines = readFileSync(manifestPath, 'utf8').trim().split(/\r?\n/).map(JSON.parse);
assert.equal(manifestLines.length, 1);
assert.equal(manifestLines[0].status, 'failed');
assert.equal(manifestLines[0].billable, false);
assert.equal(manifestLines[0].error.code, 'page_sse_request_rejected');
assert.equal(manifestLines[0].routing.transport, 'page_sse');
assert.equal(manifestLines[0].summary.ok, false);
assert.equal(manifestLines[0].summary.billable, false);
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'POST /api/images']
);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('re-fetches batch page SSE capabilities after a transient capabilities failure', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-page-sse-capability-retry-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
const manifestPath = join(tempRoot, 'manifest.jsonl');
const imagePath = join(tempRoot, 'source.png');
writeFileSync(imagePath, fakePngBuffer(2, 1));
writeFileSync(
inputPath,
JSON.stringify({
mode: 'edit',
id: 'edit-large-capability-retry',
prompt: 'edit prompt',
image_path: imagePath,
size: '3072x2048',
idempotency_key: 'capability-retry-key'
})
);
let capabilitiesRequests = 0;
const requests = [];
await withServer(
(request, response) => {
requests.push({ method: request.method, url: request.url });
if (request.url === '/api/agent/capabilities') {
capabilitiesRequests += 1;
if (capabilitiesRequests === 1) {
response.writeHead(503, { 'content-type': 'text/plain' });
response.end('capabilities maintenance');
return;
}
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: {
page_sse: { supported: true, endpoint: '/api/images' }
}
})
);
return;
}
if (request.url === '/api/images') {
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
'data: {"type":"completed","filename":"retry-edit.png","path":"/generated/retry-edit.png","output_format":"png"}',
'',
'data: {"type":"done"}',
'',
''
].join('\n')
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath, '--manifest', manifestPath, '--max-attempts', '2'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
['GET /api/agent/capabilities', 'GET /api/agent/capabilities', 'POST /api/images']
);
const body = JSON.parse(result.stdout);
assert.equal(body.results[0].attempt, 2);
assert.equal(body.results[0].root_idempotency_key, 'capability-retry-key');
assert.equal(body.results[0].response.images[0].filename, 'retry-edit.png');
const manifestLines = readFileSync(manifestPath, 'utf8').trim().split(/\r?\n/).map(JSON.parse);
assert.equal(manifestLines.length, 2);
assert.equal(manifestLines[0].status, 'failed');
assert.equal(manifestLines[0].idempotency_key, 'capability-retry-key');
assert.equal(manifestLines[1].status, 'succeeded');
assert.equal(manifestLines[1].idempotency_key, 'capability-retry-key-attempt-2');
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('retries batch failures with fresh attempt idempotency keys and reports a fix list', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-retry-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
const manifestPath = join(tempRoot, 'manifest.jsonl');
writeFileSync(
inputPath,
JSON.stringify({
id: 'retry-generate',
prompt: 'prompt',
idempotency_key: 'retry-key'
})
);
const idempotencyKeys = [];
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/image-requests') {
idempotencyKeys.push(request.headers['idempotency-key']);
response.writeHead(500, { 'content-type': 'application/json' });
response.end(
JSON.stringify({ error: { message: 'upstream failed', code: 'upstream_failed' } })
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath, '--manifest', manifestPath, '--max-attempts', '2'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stderr.trim(), '');
assert.deepEqual(idempotencyKeys, ['retry-key', 'retry-key-attempt-2']);
const body = JSON.parse(result.stdout);
assert.equal(body.failure_summary.count, 1);
assert.equal(body.failure_summary.tasks[0].id, 'retry-generate');
assert.equal(body.resume_fix_list[0].previous_idempotency_key, 'retry-key');
assert.equal(body.resume_fix_list[0].suggested_idempotency_key, 'retry-key-attempt-3');
assert.equal(body.results[0].attempt, 2);
assert.equal(body.results[0].root_idempotency_key, 'retry-key');
const manifestLines = readFileSync(manifestPath, 'utf8').trim().split(/\r?\n/).map(JSON.parse);
assert.equal(manifestLines.length, 2);
assert.equal(manifestLines[0].idempotency_key, 'retry-key');
assert.equal(manifestLines[0].attempt, 1);
assert.equal(manifestLines[1].idempotency_key, 'retry-key-attempt-2');
assert.equal(manifestLines[1].root_idempotency_key, 'retry-key');
assert.equal(manifestLines[1].attempt, 2);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('preserves the retry suffix when long batch idempotency keys are truncated', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-retry-long-key-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
const manifestPath = join(tempRoot, 'manifest.jsonl');
const longKey = 'k'.repeat(198);
writeFileSync(
inputPath,
JSON.stringify({
id: 'retry-generate-long-key',
prompt: 'prompt',
idempotency_key: longKey
})
);
const idempotencyKeys = [];
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/image-requests') {
idempotencyKeys.push(request.headers['idempotency-key']);
response.writeHead(500, { 'content-type': 'application/json' });
response.end(
JSON.stringify({ error: { message: 'upstream failed', code: 'upstream_failed' } })
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath, '--manifest', manifestPath, '--max-attempts', '3'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(idempotencyKeys.length, 3);
assert.equal(idempotencyKeys[1].length, 200);
assert.equal(idempotencyKeys[1].endsWith('-attempt-2'), true);
assert.equal(idempotencyKeys[2].length, 200);
assert.equal(idempotencyKeys[2].endsWith('-attempt-3'), true);
assert.notEqual(idempotencyKeys[1], idempotencyKeys[2]);
const body = JSON.parse(result.stdout);
assert.equal(body.resume_fix_list[0].suggested_idempotency_key.endsWith('-attempt-4'), true);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('keeps truncated batch retry idempotency keys distinct when long roots share a prefix', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-retry-collision-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
const manifestPath = join(tempRoot, 'manifest.jsonl');
const sharedPrefix = 'shared-key-'.padEnd(199, 'x');
const firstKey = `${sharedPrefix}a`;
const secondKey = `${sharedPrefix}b`;
writeFileSync(
inputPath,
[
JSON.stringify({ id: 'first-long-key', prompt: 'first', idempotency_key: firstKey }),
JSON.stringify({ id: 'second-long-key', prompt: 'second', idempotency_key: secondKey })
].join('\n')
);
const idempotencyKeys = [];
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/image-requests') {
idempotencyKeys.push(request.headers['idempotency-key']);
response.writeHead(500, { 'content-type': 'application/json' });
response.end(
JSON.stringify({ error: { message: 'upstream failed', code: 'upstream_failed' } })
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--input', inputPath, '--manifest', manifestPath, '--max-attempts', '2'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(idempotencyKeys.length, 4);
assert.equal(idempotencyKeys[1].length, 200);
assert.equal(idempotencyKeys[3].length, 200);
assert.equal(idempotencyKeys[1].endsWith('-attempt-2'), true);
assert.equal(idempotencyKeys[3].endsWith('-attempt-2'), true);
assert.notEqual(idempotencyKeys[1], idempotencyKeys[3]);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('stops batch execution after max consecutive failures and leaves later tasks resumable', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-circuit-breaker-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
const manifestPath = join(tempRoot, 'manifest.jsonl');
writeFileSync(
inputPath,
[
JSON.stringify({ id: 'first-fail', prompt: 'first', idempotency_key: 'first-key' }),
JSON.stringify({ id: 'second-skip', prompt: 'second', idempotency_key: 'second-key' })
].join('\n')
);
const idempotencyKeys = [];
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/image-requests') {
idempotencyKeys.push(request.headers['idempotency-key']);
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'upstream failed' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
[
'--allow-billable',
'--input',
inputPath,
'--manifest',
manifestPath,
'--max-consecutive-failures',
'1'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stderr.trim(), '');
assert.deepEqual(idempotencyKeys, ['first-key']);
const body = JSON.parse(result.stdout);
assert.equal(body.failed, 1);
assert.equal(body.results[0].status, 'failed');
assert.equal(body.results[1].status, 'skipped');
assert.equal(body.results[1].skipped_reason, 'max_consecutive_failures');
assert.equal(body.results[1].billable, false);
assert.equal(body.failure_summary.count, 1);
const manifestLines = readFileSync(manifestPath, 'utf8').trim().split(/\r?\n/).map(JSON.parse);
assert.equal(manifestLines.length, 2);
assert.equal(manifestLines[0].status, 'failed');
assert.equal(manifestLines[1].status, 'skipped');
assert.equal(manifestLines[1].skipped_reason, 'max_consecutive_failures');
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('checks batch output dimensions from same-origin artifact URLs', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
writeFileSync(inputPath, JSON.stringify({ id: 'dim-ok', prompt: 'prompt', size: '1024x1024' }));
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/image-requests') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
images: [{ id: 'dim-image', filename: 'dim.png', content_url: '/artifact/dim.png' }]
})
);
return;
}
if (request.url === '/artifact/dim.png') {
response.writeHead(200, { 'content-type': 'image/png' });
response.end(fakePngBuffer(1024, 1024));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--dimension-check', '--input', inputPath],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.results[0].status, 'succeeded');
assert.deepEqual(body.results[0].summary.actual_dimensions, { width: 1024, height: 1024 });
assert.deepEqual(body.results[0].summary.image_dimensions, [{ width: 1024, height: 1024 }]);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('checks batch page SSE path-mode dimensions from raw SSE image bytes before formatting', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-page-sse-dim-'));
try {
const inputPath = join(tempRoot, 'tasks.jsonl');
const finalImageBase64 = fakePngBase64(1024, 1024);
let imageRouteHits = 0;
writeFileSync(
inputPath,
JSON.stringify({ id: 'dim-page-sse', prompt: 'prompt', size: '1024x1024', page_sse: true })
);
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
agent_streaming: { page_sse: { supported: true, endpoint: '/api/images' } }
})
);
return;
}
if (request.url === '/api/images') {
await readRequestText(request);
response.writeHead(200, { 'content-type': 'text/event-stream' });
response.end(
[
`data: ${JSON.stringify({
type: 'completed',
filename: 'batch.png',
b64_json: finalImageBase64,
path: '/api/image/batch.png',
output_format: 'png'
})}`,
'',
'data: {"type":"done","images":[{"filename":"batch.png","path":"/api/image/batch.png"}]}',
'',
''
].join('\n')
);
return;
}
if (request.url === '/api/image/batch.png') {
imageRouteHits += 1;
response.writeHead(401, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'cookie required' }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--dimension-check', '--input', inputPath],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
assert.equal(imageRouteHits, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.results[0].status, 'succeeded');
assert.equal(body.results[0].routing.transport, 'page_sse');
assert.equal('b64_json' in body.results[0].response.images[0], false);
assert.equal(body.results[0].response.images[0].path, '/api/image/batch.png');
assert.equal(body.results[0].response.images[0].absolute_path, `${baseUrl}/api/image/batch.png`);
assert.deepEqual(body.results[0].response.images[0].dimensions, { width: 1024, height: 1024 });
assert.deepEqual(body.results[0].summary.actual_dimensions, { width: 1024, height: 1024 });
assert.deepEqual(body.results[0].summary.image_dimensions, [{ width: 1024, height: 1024 }]);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('fails batch dimension-check mismatches and invalid size parameters', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'gpt-image-batch-'));
try {
const invalidInputPath = join(tempRoot, 'invalid.jsonl');
writeFileSync(invalidInputPath, JSON.stringify({ id: 'bad-size', prompt: 'prompt', size: 'wide' }));
const invalidResult = runSkillScript('batch-images.mjs', ['--input', invalidInputPath]);
assert.equal(invalidResult.status, 2);
assert.match(invalidResult.stderr, /bad-size\.size 必须是 auto 或 WIDTHxHEIGHT/);
assert.equal(invalidResult.stdout.trim(), '');
const nonPositiveInputPath = join(tempRoot, 'non-positive.jsonl');
writeFileSync(
nonPositiveInputPath,
JSON.stringify({ id: 'non-positive-size', prompt: 'prompt', size: '0x3840' })
);
const nonPositiveResult = runSkillScript('batch-images.mjs', ['--input', nonPositiveInputPath]);
assert.equal(nonPositiveResult.status, 2);
assert.match(nonPositiveResult.stderr, /non-positive-size\.size 的宽度和高度必须是正数/);
assert.equal(nonPositiveResult.stdout.trim(), '');
const invalidModelInputPath = join(tempRoot, 'invalid-model.jsonl');
writeFileSync(
invalidModelInputPath,
JSON.stringify({
id: 'invalid-model',
prompt: 'prompt',
model: 'bad-model',
size: '2048x2048'
})
);
const invalidModelResult = runSkillScript('batch-images.mjs', ['--input', invalidModelInputPath]);
assert.equal(invalidModelResult.status, 2);
assert.match(invalidModelResult.stderr, /invalid-model\.model 的值无效:bad-model/);
assert.equal(invalidModelResult.stdout.trim(), '');
const pngCompressionInputPath = join(tempRoot, 'png-compression.jsonl');
writeFileSync(
pngCompressionInputPath,
JSON.stringify({
id: 'png-compression',
prompt: 'prompt',
output_format: 'png',
output_compression: 80
})
);
const pngCompressionResult = runSkillScript('batch-images.mjs', ['--input', pngCompressionInputPath]);
assert.equal(pngCompressionResult.status, 0);
assert.equal(pngCompressionResult.stderr.trim(), '');
const pngCompressionBody = JSON.parse(pngCompressionResult.stdout);
assert.equal(pngCompressionBody.tasks[0].request.normalizations.output_compression_ignored_for_png, true);
assert.equal(pngCompressionBody.tasks[0].request.output_compression, undefined);
const conflictingFormatInputPath = join(tempRoot, 'conflicting-format.jsonl');
writeFileSync(
conflictingFormatInputPath,
JSON.stringify({
id: 'conflicting-format',
prompt: 'prompt',
output_format: 'png',
format: 'webp'
})
);
const conflictingFormatResult = runSkillScript('batch-images.mjs', ['--input', conflictingFormatInputPath]);
assert.equal(conflictingFormatResult.status, 2);
assert.match(conflictingFormatResult.stderr, /conflicting-format\.output_format 与 format 不能同时设置/);
assert.equal(conflictingFormatResult.stdout.trim(), '');
const editFormatInputPath = join(tempRoot, 'edit-format.jsonl');
writeFileSync(
editFormatInputPath,
JSON.stringify({
id: 'edit-format',
mode: 'edit',
prompt: 'prompt',
image_path: 'source.png',
size: '1024x1024',
output_format: 'jpeg'
})
);
const editFormatResult = runSkillScript('batch-images.mjs', ['--input', editFormatInputPath]);
assert.equal(editFormatResult.status, 0);
assert.equal(editFormatResult.stderr.trim(), '');
const editFormatBody = JSON.parse(editFormatResult.stdout);
assert.equal(editFormatBody.tasks[0].routing.transport, 'page_sse');
assert.match(editFormatBody.tasks[0].routing.reason, /GPT2Image-compatible edit options/);
assert.equal(editFormatBody.tasks[0].request.output_format, 'jpeg');
const editBackendInputPath = join(tempRoot, 'edit-backend.jsonl');
writeFileSync(
editBackendInputPath,
JSON.stringify({
id: 'edit-backend',
mode: 'edit',
prompt: 'prompt',
image_path: 'source.png',
image_backend: 'responses-image-generation'
})
);
const editBackendResult = runSkillScript('batch-images.mjs', ['--input', editBackendInputPath]);
assert.equal(editBackendResult.status, 0);
assert.equal(editBackendResult.stderr.trim(), '');
const editBackendBody = JSON.parse(editBackendResult.stdout);
assert.equal(editBackendBody.tasks[0].routing.transport, 'page_sse');
assert.equal(editBackendBody.tasks[0].request.image_backend, 'responses-image-generation');
const editBackgroundInputPath = join(tempRoot, 'edit-background.jsonl');
writeFileSync(
editBackgroundInputPath,
JSON.stringify({
id: 'edit-background',
mode: 'edit',
prompt: 'prompt',
image_path: 'source.png',
background: 'opaque'
})
);
const editBackgroundResult = runSkillScript('batch-images.mjs', ['--input', editBackgroundInputPath]);
assert.equal(editBackgroundResult.status, 2);
assert.match(editBackgroundResult.stderr, /edit-background\.background 仅适用于 generate 任务/);
assert.equal(editBackgroundResult.stdout.trim(), '');
const editResponsesModelInputPath = join(tempRoot, 'edit-responses-model.jsonl');
writeFileSync(
editResponsesModelInputPath,
JSON.stringify({
id: 'edit-responses-model',
mode: 'edit',
prompt: 'prompt',
image_path: 'source.png',
responsesModel: 'gpt-4.1'
})
);
const editResponsesModelResult = runSkillScript('batch-images.mjs', [
'--input',
editResponsesModelInputPath
]);
assert.equal(editResponsesModelResult.status, 2);
assert.match(
editResponsesModelResult.stderr,
/edit-responses-model\.responsesModel 必须同时设置 image_backend=responses-image-generation/
);
assert.equal(editResponsesModelResult.stdout.trim(), '');
const generateImagePathInputPath = join(tempRoot, 'generate-image-path.jsonl');
writeFileSync(
generateImagePathInputPath,
JSON.stringify({
id: 'generate-image-path',
mode: 'generate',
prompt: 'prompt',
image_path: 'source.png'
})
);
const generateImagePathResult = runSkillScript('batch-images.mjs', ['--input', generateImagePathInputPath]);
assert.equal(generateImagePathResult.status, 2);
assert.match(generateImagePathResult.stderr, /generate-image-path\.image_path 仅适用于 edit 任务/);
assert.equal(generateImagePathResult.stdout.trim(), '');
const camelCaseBackendInputPath = join(tempRoot, 'camel-case-backend.jsonl');
writeFileSync(
camelCaseBackendInputPath,
JSON.stringify({
id: 'camel-case-backend',
prompt: 'prompt',
imageBackend: 'responses'
})
);
const camelCaseBackendResult = runSkillScript('batch-images.mjs', ['--input', camelCaseBackendInputPath]);
assert.equal(camelCaseBackendResult.status, 2);
assert.match(camelCaseBackendResult.stderr, /camel-case-backend\.imageBackend 不是支持的 batch JSONL 字段/);
assert.equal(camelCaseBackendResult.stdout.trim(), '');
const formFieldStreamingInputPath = join(tempRoot, 'form-field-streaming.jsonl');
writeFileSync(
formFieldStreamingInputPath,
JSON.stringify({
id: 'form-field-streaming',
prompt: 'prompt',
image_backend: 'responses-image-generation',
image_streaming_strategy: 'responses-sse'
})
);
const formFieldStreamingResult = runSkillScript('batch-images.mjs', [
'--input',
formFieldStreamingInputPath
]);
assert.equal(formFieldStreamingResult.status, 2);
assert.match(
formFieldStreamingResult.stderr,
/form-field-streaming\.image_streaming_strategy 不是支持的 batch JSONL 字段/
);
assert.equal(formFieldStreamingResult.stdout.trim(), '');
const camelCaseResponseModeInputPath = join(tempRoot, 'camel-case-response-mode.jsonl');
writeFileSync(
camelCaseResponseModeInputPath,
JSON.stringify({
id: 'camel-case-response-mode',
prompt: 'prompt',
responseMode: 'both'
})
);
const camelCaseResponseModeResult = runSkillScript('batch-images.mjs', [
'--input',
camelCaseResponseModeInputPath
]);
assert.equal(camelCaseResponseModeResult.status, 2);
assert.match(
camelCaseResponseModeResult.stderr,
/camel-case-response-mode\.responseMode 不是支持的 batch JSONL 字段/
);
assert.equal(camelCaseResponseModeResult.stdout.trim(), '');
const numericImagePathInputPath = join(tempRoot, 'numeric-image-path.jsonl');
writeFileSync(
numericImagePathInputPath,
JSON.stringify({
id: 'numeric-image-path',
mode: 'edit',
prompt: 'prompt',
image_path: 123
})
);
const numericImagePathResult = runSkillScript('batch-images.mjs', ['--input', numericImagePathInputPath]);
assert.equal(numericImagePathResult.status, 2);
assert.match(numericImagePathResult.stderr, /numeric-image-path\.image_path 必须是非空字符串/);
assert.equal(numericImagePathResult.stdout.trim(), '');
const emptyImagePathsInputPath = join(tempRoot, 'empty-image-paths.jsonl');
writeFileSync(
emptyImagePathsInputPath,
JSON.stringify({
id: 'empty-image-paths',
mode: 'edit',
prompt: 'prompt',
image_paths: []
})
);
const emptyImagePathsResult = runSkillScript('batch-images.mjs', ['--input', emptyImagePathsInputPath]);
assert.equal(emptyImagePathsResult.status, 2);
assert.match(emptyImagePathsResult.stderr, /empty-image-paths\.image_paths 必须是非空字符串数组/);
assert.equal(emptyImagePathsResult.stdout.trim(), '');
const invalidImagePathsInputPath = join(tempRoot, 'invalid-image-paths.jsonl');
writeFileSync(
invalidImagePathsInputPath,
JSON.stringify({
id: 'invalid-image-paths',
mode: 'edit',
prompt: 'prompt',
image_paths: ['source.png', 123]
})
);
const invalidImagePathsResult = runSkillScript('batch-images.mjs', ['--input', invalidImagePathsInputPath]);
assert.equal(invalidImagePathsResult.status, 2);
assert.match(invalidImagePathsResult.stderr, /invalid-image-paths\.image_paths\[1\] 必须是非空字符串/);
assert.equal(invalidImagePathsResult.stdout.trim(), '');
const conflictingImagePathsInputPath = join(tempRoot, 'conflicting-image-paths.jsonl');
writeFileSync(
conflictingImagePathsInputPath,
JSON.stringify({
id: 'conflicting-image-paths',
mode: 'edit',
prompt: 'prompt',
image_path: 'source.png',
image_paths: ['source-a.png']
})
);
const conflictingImagePathsResult = runSkillScript('batch-images.mjs', [
'--input',
conflictingImagePathsInputPath
]);
assert.equal(conflictingImagePathsResult.status, 2);
assert.match(
conflictingImagePathsResult.stderr,
/conflicting-image-paths\.image_path 与 image_paths 不能同时设置/
);
assert.equal(conflictingImagePathsResult.stdout.trim(), '');
const invalidMaskInputPath = join(tempRoot, 'invalid-mask.jsonl');
writeFileSync(
invalidMaskInputPath,
JSON.stringify({
id: 'invalid-mask',
mode: 'edit',
prompt: 'prompt',
image_path: 'source.png',
mask_path: {}
})
);
const invalidMaskResult = runSkillScript('batch-images.mjs', ['--input', invalidMaskInputPath]);
assert.equal(invalidMaskResult.status, 2);
assert.match(invalidMaskResult.stderr, /invalid-mask\.mask_path 必须是非空字符串/);
assert.equal(invalidMaskResult.stdout.trim(), '');
const transparentInputPath = join(tempRoot, 'transparent.jsonl');
writeFileSync(
transparentInputPath,
JSON.stringify({
id: 'transparent-background',
prompt: 'prompt',
background: 'transparent'
})
);
const transparentResult = runSkillScript('batch-images.mjs', ['--input', transparentInputPath]);
assert.equal(transparentResult.status, 0);
assert.equal(transparentResult.stderr.trim(), '');
const transparentBody = JSON.parse(transparentResult.stdout);
assert.equal(transparentBody.tasks[0].request.background, 'transparent');
const stringPageSseInputPath = join(tempRoot, 'string-page-sse.jsonl');
writeFileSync(
stringPageSseInputPath,
JSON.stringify({
id: 'string-page-sse',
prompt: 'prompt',
page_sse: 'true'
})
);
const stringPageSseResult = runSkillScript('batch-images.mjs', ['--input', stringPageSseInputPath]);
assert.equal(stringPageSseResult.status, 2);
assert.match(stringPageSseResult.stderr, /string-page-sse\.page_sse 必须是布尔值/);
assert.equal(stringPageSseResult.stdout.trim(), '');
const stringComplexUiInputPath = join(tempRoot, 'string-complex-ui.jsonl');
writeFileSync(
stringComplexUiInputPath,
JSON.stringify({
id: 'string-complex-ui',
prompt: 'prompt',
complex_ui: 'true'
})
);
const stringComplexUiResult = runSkillScript('batch-images.mjs', ['--input', stringComplexUiInputPath]);
assert.equal(stringComplexUiResult.status, 2);
assert.match(stringComplexUiResult.stderr, /string-complex-ui\.complex_ui 必须是布尔值/);
assert.equal(stringComplexUiResult.stdout.trim(), '');
const numericResumeInputPath = join(tempRoot, 'numeric-resume.jsonl');
writeFileSync(
numericResumeInputPath,
JSON.stringify({
id: 'numeric-resume',
prompt: 'prompt',
resume_or_recover: 1
})
);
const numericResumeResult = runSkillScript('batch-images.mjs', ['--input', numericResumeInputPath]);
assert.equal(numericResumeResult.status, 2);
assert.match(numericResumeResult.stderr, /numeric-resume\.resume_or_recover 必须是布尔值/);
assert.equal(numericResumeResult.stdout.trim(), '');
const unsupportedTransportInputPath = join(tempRoot, 'unsupported-transport.jsonl');
writeFileSync(
unsupportedTransportInputPath,
JSON.stringify({
id: 'unsupported-transport',
prompt: 'prompt',
transport: 'agent_json'
})
);
const unsupportedTransportResult = runSkillScript('batch-images.mjs', [
'--input',
unsupportedTransportInputPath
]);
assert.equal(unsupportedTransportResult.status, 2);
assert.match(unsupportedTransportResult.stderr, /unsupported-transport\.transport 必须是 page_sse/);
assert.equal(unsupportedTransportResult.stdout.trim(), '');
const responsesModelNonStreamInputPath = join(tempRoot, 'responses-model-non-stream.jsonl');
writeFileSync(
responsesModelNonStreamInputPath,
JSON.stringify({
id: 'responses-model-non-stream',
prompt: 'prompt',
image_backend: 'responses-image-generation',
responsesModel: 'gpt-4.1',
stream_mode: 'non_stream'
})
);
const responsesModelNonStreamResult = runSkillScript('batch-images.mjs', [
'--input',
responsesModelNonStreamInputPath
]);
assert.equal(responsesModelNonStreamResult.status, 0);
assert.equal(responsesModelNonStreamResult.stderr.trim(), '');
const responsesModelNonStreamBody = JSON.parse(responsesModelNonStreamResult.stdout);
assert.equal(responsesModelNonStreamBody.tasks[0].routing.transport, 'server_orchestrated');
assert.equal(responsesModelNonStreamBody.tasks[0].routing.endpoint, '/api/agent/image-requests');
assert.equal(responsesModelNonStreamBody.tasks[0].request.responsesModel, 'gpt-4.1');
assert.equal(responsesModelNonStreamBody.tasks[0].request.stream_mode, 'non_stream');
const responsesModelWithoutBackendInputPath = join(tempRoot, 'responses-model-without-backend.jsonl');
writeFileSync(
responsesModelWithoutBackendInputPath,
JSON.stringify({
id: 'responses-model-without-backend',
prompt: 'prompt',
responsesModel: 'gpt-4.1'
})
);
const responsesModelWithoutBackendResult = runSkillScript('batch-images.mjs', [
'--input',
responsesModelWithoutBackendInputPath
]);
assert.equal(responsesModelWithoutBackendResult.status, 2);
assert.match(
responsesModelWithoutBackendResult.stderr,
/responses-model-without-backend\.responsesModel 必须同时设置 image_backend=responses-image-generation/
);
assert.equal(responsesModelWithoutBackendResult.stdout.trim(), '');
const responsesModelImagesBackendInputPath = join(tempRoot, 'responses-model-images-backend.jsonl');
writeFileSync(
responsesModelImagesBackendInputPath,
JSON.stringify({
id: 'responses-model-images-backend',
prompt: 'prompt',
image_backend: 'images-api',
responsesModel: 'gpt-4.1'
})
);
const responsesModelImagesBackendResult = runSkillScript('batch-images.mjs', [
'--input',
responsesModelImagesBackendInputPath
]);
assert.equal(responsesModelImagesBackendResult.status, 2);
assert.match(
responsesModelImagesBackendResult.stderr,
/responses-model-images-backend\.responsesModel 仅适用于 image_backend=responses-image-generation/
);
assert.equal(responsesModelImagesBackendResult.stdout.trim(), '');
const mismatchInputPath = join(tempRoot, 'mismatch.jsonl');
writeFileSync(mismatchInputPath, JSON.stringify({ id: 'dim-bad', prompt: 'prompt', size: '1024x1024' }));
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ ok: true }));
return;
}
if (request.url === '/api/agent/image-requests') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
execution: {
transport: 'agent_job_polling',
endpoint: '/api/agent/image-requests',
route_mode: 'job',
channel_request_mode: 'images-non-stream',
channel_request_mode_fallback_applied: true,
route_decision: {
requested_backend: 'images-api',
preferred_channel_request_mode: 'images-sse',
fallback_channel_request_mode: 'images-non-stream',
selected_channel_request_mode: 'images-non-stream'
},
selected_channel_id: 'dimension-channel',
upstream_host: 'dimension-upstream.example.test'
},
images: [
{ id: 'dim-image-bad', filename: 'dim-bad.png', b64_json: fakePngBase64(512, 512) },
{ id: 'dim-image-ok', filename: 'dim-ok.png', b64_json: fakePngBase64(1024, 1024) }
]
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'batch-images.mjs',
['--allow-billable', '--dimension-check', '--input', mismatchInputPath],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.ok, false);
assert.equal(body.results[0].error.code, 'dimension_check_failed');
assert.match(body.results[0].error.message, /尺寸校验失败/);
assert.deepEqual(body.results[0].error.expected_dimensions, { width: 1024, height: 1024 });
assert.deepEqual(body.results[0].error.actual_dimensions, { width: 512, height: 512 });
assert.equal(body.results[0].validation_failure_kind, 'generated_artifact_failed_dimension_check');
assert.equal(body.results[0].response.images[0].b64_json, undefined);
assert.equal(body.results[0].response.images[0].b64_json_length, fakePngBase64(512, 512).length);
assert.deepEqual(body.results[0].response.images[0].dimensions, { width: 512, height: 512 });
assert.equal(body.results[0].response.images[1].b64_json, undefined);
assert.equal(body.results[0].response.images[1].b64_json_length, fakePngBase64(1024, 1024).length);
assert.deepEqual(body.results[0].response.images[1].dimensions, { width: 1024, height: 1024 });
assert.deepEqual(body.results[0].summary.artifact_ids, ['dim-image-bad', 'dim-image-ok']);
assert.deepEqual(body.results[0].summary.image_dimensions, [
{ width: 512, height: 512 },
{ width: 1024, height: 1024 }
]);
assert.deepEqual(body.results[0].summary.expected_dimensions, { width: 1024, height: 1024 });
assert.deepEqual(body.results[0].summary.actual_dimensions, { width: 512, height: 512 });
assert.equal(body.results[0].summary.dimension_check_failed, true);
assert.equal(body.results[0].summary.transport, 'agent_job_polling');
assert.equal(body.results[0].summary.endpoint, '/api/agent/image-requests');
assert.equal(body.results[0].summary.route_mode, 'job');
assert.equal(body.results[0].summary.channel_request_mode, 'images-non-stream');
assert.equal(body.results[0].summary.channel_request_mode_fallback_applied, true);
assert.deepEqual(body.results[0].summary.route_decision, {
requested_backend: 'images-api',
preferred_channel_request_mode: 'images-sse',
fallback_channel_request_mode: 'images-non-stream',
selected_channel_request_mode: 'images-non-stream'
});
assert.equal(body.results[0].summary.selected_channel_id, 'dimension-channel');
assert.equal(body.results[0].summary.upstream_host, 'dimension-upstream.example.test');
assert.equal(body.failure_summary.validation_failure_count, 1);
assert.equal(body.failure_summary.request_failure_count, 0);
assert.equal(
body.failure_summary.tasks[0].failure_kind,
'generated_artifact_failed_dimension_check'
);
assert.deepEqual(body.failure_summary.tasks[0].artifact_ids, ['dim-image-bad', 'dim-image-ok']);
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('rejects cross-origin job result URLs before sending auth headers', () => {
assert.throws(
() =>
resolveSameOriginUrl(
'https://space.example.test',
'https://evil.example.test/result',
'job.result_url'
),
/不同 origin/
);
assert.equal(
resolveSameOriginUrl('https://space.example.test', '/api/agent/jobs/abc/result', 'job.result_url'),
'https://space.example.test/api/agent/jobs/abc/result'
);
});
it('caps retry-after values before sleeping', () => {
assert.equal(parseRetryAfterValue('5'), 5);
assert.equal(parseRetryAfterValue('0'), 1);
assert.equal(parseRetryAfterValue('999999999999999999999'), 60);
assert.equal(parseRetryAfterValue('not-a-number', 7), 7);
});
it('preserves non-JSON capabilities status and body in generate failures', async () => {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(503, { 'content-type': 'text/plain' });
response.end('maintenance window');
return;
}
response.writeHead(404, { 'content-type': 'text/plain' });
response.end('missing');
},
async (baseUrl) => {
const result = await runSkillScriptAsync('generate-image.mjs', ['--allow-billable', 'prompt'], {
GPT_IMAGE_PLAYGROUND_URL: baseUrl
});
assert.equal(result.status, 1);
assert.match(result.stderr, /capabilities 请求失败,状态码 503:maintenance window/);
}
);
});
it('checks single generate output dimensions from same-origin artifact URLs', async () => {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify(agentGenerateCapabilities()));
return;
}
if (request.url === '/api/agent/image-requests' && request.method === 'POST') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
job: {
id: 'job-dimension-ok',
state: 'succeeded',
result_url: '/api/agent/jobs/job-dimension-ok/result'
}
})
);
return;
}
if (request.url === '/api/agent/jobs/job-dimension-ok/result') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
request_id: 'request-dimension-ok',
idempotency_key: 'dimension-ok-key',
images: [
{
id: 'artifact-dimension-ok',
content_url: '/artifact/dimension-ok.png'
}
]
})
);
return;
}
if (request.url === '/artifact/dimension-ok.png') {
response.writeHead(200, { 'content-type': 'image/png' });
response.end(fakePngBuffer(1024, 1024));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--dimension-check',
'--size',
'1024x1024',
'--idempotency-key',
'dimension-ok-key',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.deepEqual(body.images[0].dimensions, { width: 1024, height: 1024 });
assert.deepEqual(body.summary.actual_dimensions, { width: 1024, height: 1024 });
assert.deepEqual(body.summary.image_dimensions, [{ width: 1024, height: 1024 }]);
}
);
});
it('fails single generate dimension-check mismatches with route diagnostics', async () => {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify(agentGenerateCapabilities()));
return;
}
if (request.url === '/api/agent/image-requests' && request.method === 'POST') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
job: {
id: 'job-dimension-bad',
state: 'succeeded',
result_url: '/api/agent/jobs/job-dimension-bad/result'
}
})
);
return;
}
if (request.url === '/api/agent/jobs/job-dimension-bad/result') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
request_id: 'request-dimension-bad',
idempotency_key: 'dimension-bad-key',
execution: {
transport: 'agent_job_polling',
endpoint: '/api/agent/image-requests',
route_mode: 'job',
channel_request_mode: 'images-non-stream',
channel_request_mode_fallback_applied: true,
route_decision: {
requested_backend: 'images-api',
preferred_channel_request_mode: 'images-sse',
fallback_channel_request_mode: 'images-non-stream',
selected_channel_request_mode: 'images-non-stream'
},
selected_channel_id: 'single-dimension-channel',
upstream_host: 'single-dimension-upstream.example.test'
},
images: [
{
id: 'artifact-dimension-bad',
b64_json: fakePngBase64(1254, 1254)
}
]
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--dimension-check',
'--size',
'1024x1024',
'--idempotency-key',
'dimension-bad-key',
'prompt'
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const body = JSON.parse(result.stderr);
assert.equal(body.error.code, 'dimension_check_failed');
assert.equal(body.validation_failure_kind, 'generated_artifact_failed_dimension_check');
assert.deepEqual(body.error.expected_dimensions, { width: 1024, height: 1024 });
assert.deepEqual(body.error.actual_dimensions, { width: 1254, height: 1254 });
assert.equal(body.response.images[0].b64_json, undefined);
assert.equal(body.response.images[0].b64_json_length, fakePngBase64(1254, 1254).length);
assert.deepEqual(body.response.images[0].dimensions, { width: 1254, height: 1254 });
assert.equal(body.summary.dimension_check_failed, true);
assert.equal(body.summary.transport, 'agent_job_polling');
assert.equal(body.summary.endpoint, '/api/agent/image-requests');
assert.equal(body.summary.route_mode, 'job');
assert.equal(body.summary.channel_request_mode, 'images-non-stream');
assert.equal(body.summary.channel_request_mode_fallback_applied, true);
assert.deepEqual(body.summary.route_decision, {
requested_backend: 'images-api',
preferred_channel_request_mode: 'images-sse',
fallback_channel_request_mode: 'images-non-stream',
selected_channel_request_mode: 'images-non-stream'
});
assert.equal(body.summary.selected_channel_id, 'single-dimension-channel');
assert.equal(body.summary.upstream_host, 'single-dimension-upstream.example.test');
assert.match(body.summary.next_action, /确认当前渠道是否支持请求尺寸/);
}
);
});
it('creates artifact share links through the Agent share endpoint when requested', async () => {
const requests = [];
await withServer(
async (request, response) => {
requests.push({
method: request.method,
url: request.url,
authorization: request.headers.authorization
});
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify(agentGenerateCapabilities()));
return;
}
if (request.url === '/api/agent/image-requests' && request.method === 'POST') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
job: {
id: 'job-share',
state: 'succeeded',
result_url: '/api/agent/jobs/job-share/result'
}
})
);
return;
}
if (request.url === '/api/agent/jobs/job-share/result') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
request_id: 'request-share',
idempotency_key: 'share-key',
cached: false,
images: [
{
id: 'artifact-share',
content_url: '/api/agent/artifacts/artifact-share/content',
metadata_url: '/api/agent/artifacts/artifact-share',
output_format: 'png',
mime_type: 'image/png',
size_bytes: 12,
width: 1024,
height: 1024
}
],
created_at: '2026-06-27T00:00:00.000Z'
})
);
return;
}
if (request.url === '/api/agent/artifacts/artifact-share/share' && request.method === 'POST') {
assert.equal(request.headers.authorization, 'Bearer share-token');
assert.deepEqual(JSON.parse(await readRequestText(request)), {
expires_in_minutes: 60,
access_code: 'share-code'
});
response.writeHead(201, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
artifact_id: 'artifact-share',
token: '0123456789abcdef01234567',
share_url: `${serverOrigin(request)}/share/0123456789abcdef01234567`,
direct_content_url: `${serverOrigin(request)}/api/shares/0123456789abcdef01234567/content`,
expires_at: '2026-06-28T00:00:00.000Z',
access_code_required: true
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
[
'--allow-billable',
'--share',
'--share-expires-minutes',
'60',
'--idempotency-key',
'share-key',
'prompt'
],
{
GPT_IMAGE_PLAYGROUND_URL: baseUrl,
GPT_IMAGE_AGENT_TOKEN: 'share-token',
GPT_IMAGE_SHARE_ACCESS_CODE: 'share-code'
}
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.shares.length, 1);
assert.equal(body.shares[0].artifact_id, 'artifact-share');
assert.equal(body.shares[0].access_code_required, true);
assert.equal(body.summary.share_urls[0], `${baseUrl}/share/0123456789abcdef01234567`);
assert.equal(
body.summary.direct_content_urls[0],
`${baseUrl}/api/shares/0123456789abcdef01234567/content`
);
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /api/agent/capabilities',
'POST /api/agent/image-requests',
'GET /api/agent/jobs/job-share/result',
'POST /api/agent/artifacts/artifact-share/share'
]
);
}
);
});
it('resolves relative share URLs against the configured playground base url', async () => {
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify(agentGenerateCapabilities()));
return;
}
if (request.url === '/api/agent/image-requests' && request.method === 'POST') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
job: {
id: 'job-relative-share',
state: 'succeeded',
result_url: '/api/agent/jobs/job-relative-share/result'
}
})
);
return;
}
if (request.url === '/api/agent/jobs/job-relative-share/result') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
request_id: 'request-relative-share',
idempotency_key: 'relative-share-key',
cached: false,
images: [
{
id: 'artifact-relative-share',
content_url: '/api/agent/artifacts/artifact-relative-share/content',
metadata_url: '/api/agent/artifacts/artifact-relative-share',
output_format: 'png',
mime_type: 'image/png',
size_bytes: 12,
width: 1024,
height: 1024
}
],
created_at: '2026-06-27T00:00:00.000Z'
})
);
return;
}
if (request.url === '/api/agent/artifacts/artifact-relative-share/share' && request.method === 'POST') {
response.writeHead(201, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
artifact_id: 'artifact-relative-share',
token: 'fedcba9876543210fedcba98',
share_url: '/share/fedcba9876543210fedcba98',
direct_content_url: '/api/shares/fedcba9876543210fedcba98/content',
expires_at: null,
access_code_required: false
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--share', '--idempotency-key', 'relative-share-key', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl, GPT_IMAGE_AGENT_TOKEN: 'share-token' }
);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.summary.share_urls[0], `${baseUrl}/share/fedcba9876543210fedcba98`);
assert.equal(
body.summary.direct_content_urls[0],
`${baseUrl}/api/shares/fedcba9876543210fedcba98/content`
);
}
);
});
it('rejects share-only generate flags without share mode', async () => {
const result = await runSkillScriptAsync('generate-image.mjs', ['--share-expires-minutes', '60', 'prompt'], {});
assert.equal(result.status, 2);
assert.match(result.stderr, /--share-expires-minutes 需要和 --share 一起使用/);
});
it('keeps share URLs under configured playground subpaths', async () => {
await withServer(
async (request, response) => {
if (request.url === '/playground/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify(agentGenerateCapabilities()));
return;
}
if (request.url === '/playground/api/agent/image-requests' && request.method === 'POST') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
job: {
id: 'job-subpath-share',
state: 'succeeded',
result_url: '/playground/api/agent/jobs/job-subpath-share/result'
}
})
);
return;
}
if (request.url === '/playground/api/agent/jobs/job-subpath-share/result') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
request_id: 'request-subpath-share',
idempotency_key: 'subpath-share-key',
cached: false,
images: [
{
id: 'artifact-subpath-share',
content_url: '/playground/api/agent/artifacts/artifact-subpath-share/content',
metadata_url: '/playground/api/agent/artifacts/artifact-subpath-share',
output_format: 'png',
mime_type: 'image/png',
size_bytes: 12,
width: 1024,
height: 1024
}
],
created_at: '2026-06-27T00:00:00.000Z'
})
);
return;
}
if (
request.url === '/playground/api/agent/artifacts/artifact-subpath-share/share' &&
request.method === 'POST'
) {
response.writeHead(201, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
artifact_id: 'artifact-subpath-share',
token: 'abcdefabcdefabcdefabcdef',
share_url: '/playground/share/abcdefabcdefabcdefabcdef',
direct_content_url: '/playground/api/shares/abcdefabcdefabcdefabcdef/content',
expires_at: null,
access_code_required: false
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--share', '--idempotency-key', 'subpath-share-key', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: `${baseUrl}/playground`, GPT_IMAGE_AGENT_TOKEN: 'share-token' }
);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.summary.share_urls[0], `${baseUrl}/playground/share/abcdefabcdefabcdefabcdef`);
assert.equal(
body.summary.direct_content_urls[0],
`${baseUrl}/playground/api/shares/abcdefabcdefabcdefabcdef/content`
);
}
);
});
it('treats blank share access code env as unset', async () => {
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify(agentGenerateCapabilities()));
return;
}
if (request.url === '/api/agent/image-requests' && request.method === 'POST') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
job: {
id: 'job-share-blank-access-code',
state: 'succeeded',
result_url: '/api/agent/jobs/job-share-blank-access-code/result'
}
})
);
return;
}
if (request.url === '/api/agent/jobs/job-share-blank-access-code/result') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
request_id: 'request-share-blank-access-code',
idempotency_key: 'share-blank-access-code-key',
cached: false,
images: [
{
id: 'artifact-share-blank-access-code',
content_url: '/api/agent/artifacts/artifact-share-blank-access-code/content',
metadata_url: '/api/agent/artifacts/artifact-share-blank-access-code',
output_format: 'png',
mime_type: 'image/png',
size_bytes: 12,
width: 1024,
height: 1024
}
],
created_at: '2026-06-27T00:00:00.000Z'
})
);
return;
}
if (
request.url === '/api/agent/artifacts/artifact-share-blank-access-code/share' &&
request.method === 'POST'
) {
response.writeHead(201, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
artifact_id: 'artifact-share-blank-access-code',
token: '0123456789abcdef01234567',
share_url: '/share/0123456789abcdef01234567',
direct_content_url: '/api/shares/0123456789abcdef01234567/content',
expires_at: null,
access_code_required: false
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--share', '--idempotency-key', 'share-blank-access-code-key', 'prompt'],
{
GPT_IMAGE_PLAYGROUND_URL: baseUrl,
GPT_IMAGE_AGENT_TOKEN: 'share-token',
GPT_IMAGE_SHARE_ACCESS_CODE: ' '
}
);
assert.equal(result.status, 0);
const body = JSON.parse(result.stdout);
assert.equal(body.shares[0].access_code_required, false);
assert.equal(body.summary.share_urls[0], `${baseUrl}/share/0123456789abcdef01234567`);
}
);
});
it('does not duplicate network failure prefixes in generate capability errors', async () => {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
return;
}
response.writeHead(404, { 'content-type': 'text/plain' });
response.end('missing');
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'generate-image.mjs',
['--allow-billable', '--timeout-ms', '50', 'prompt'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
const prefixes = result.stderr.match(/请求失败:/g) || [];
assert.equal(prefixes.length, 1);
assert.match(result.stderr, /\/api\/agent\/capabilities/);
}
);
});
it('diagnoses page request feedback and log summaries through Agent endpoints', async () => {
const requests = [];
await withServer(
async (request, response) => {
requests.push({
method: request.method,
url: request.url,
authorization: request.headers.authorization
});
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
endpoints: {
page_request_feedback_batch: '/api/agent/page-requests/feedback',
page_request_feedback: '/api/agent/page-requests/{id}/feedback',
page_request_diagnostics: '/api/agent/diagnostics/page-requests/{id}'
}
})
);
return;
}
if (request.url === '/api/agent/page-requests/feedback' && request.method === 'POST') {
assert.deepEqual(JSON.parse(await readRequestText(request)), { ids: ['web-diag'] });
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
feedback: [
{
target_type: 'page_request',
target_id: 'web-diag',
value: 'usable',
source: 'webui',
updated_at: '2026-05-12T00:00:00.000Z',
note: 'approved'
}
]
})
);
return;
}
if (request.url === '/api/agent/diagnostics/page-requests/web-diag?filename=hero.png') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
scope: {
request_ids: ['web-diag'],
filenames: ['hero.png'],
filename_matched_request_ids: [],
copy_text: 'requestIds=web-diag'
},
matched_log_count: 1,
events: [{ id: 1, at: '2026-05-12T00:00:01.000Z', level: 'info', message: 'ok' }]
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'diagnose-request.mjs',
['--client-request-id', 'web-diag', '--filename', 'hero.png'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl, GPT_IMAGE_AGENT_TOKEN: 'diag-token' }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.client_request_id, 'web-diag');
assert.equal(body.found, true);
assert.equal(body.page_request_query_count, 1);
assert.equal(body.page_request_found_count, 1);
assert.equal(body.feedback.value, 'usable');
assert.equal(body.diagnostics.matched_log_count, 1);
assert.equal(body.request_count, 1);
assert.equal(body.requests[0].client_request_id, 'web-diag');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /api/agent/capabilities',
'POST /api/agent/page-requests/feedback',
'GET /api/agent/diagnostics/page-requests/web-diag?filename=hero.png'
]
);
assert.equal(requests[1].authorization, 'Bearer diag-token');
assert.equal(requests[2].authorization, 'Bearer diag-token');
}
);
});
it('diagnoses repeated request ids and batch manifests without duplicate feedback calls', async () => {
const tempRoot = mkdtempSync(join(tmpdir(), 'diagnose-request-'));
const manifestPath = join(tempRoot, 'batch.manifest.jsonl');
const outputPath = join(tempRoot, 'diagnosis.json');
writeFileSync(
manifestPath,
[
JSON.stringify({ id: 'task-a', idempotency_key: 'web-diag-a', status: 'succeeded' }),
JSON.stringify({ id: 'task-b', client_request_id: 'web-diag-b', status: 'succeeded' })
].join('\n')
);
const requests = [];
try {
await withServer(
async (request, response) => {
requests.push({
method: request.method,
url: request.url,
authorization: request.headers.authorization
});
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
endpoints: {
page_request_feedback_batch: '/api/agent/page-requests/feedback',
page_request_diagnostics_batch: '/api/agent/diagnostics/page-requests',
page_request_diagnostics: '/api/agent/diagnostics/page-requests/{id}'
}
})
);
return;
}
if (request.url === '/api/agent/page-requests/feedback' && request.method === 'POST') {
assert.deepEqual(JSON.parse(await readRequestText(request)), {
ids: ['web-diag-a', 'web-diag-b']
});
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
feedback: [
{
target_type: 'page_request',
target_id: 'web-diag-b',
value: 'needs_revision',
source: 'webui',
updated_at: '2026-05-12T00:00:00.000Z'
}
]
})
);
return;
}
if (request.url === '/api/agent/diagnostics/page-requests' && request.method === 'POST') {
assert.deepEqual(JSON.parse(await readRequestText(request)), {
ids: ['web-diag-a', 'web-diag-b'],
filenames: ['hero.png']
});
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
diagnostics: ['web-diag-a', 'web-diag-b'].map((requestId) => ({
client_request_id: requestId,
scope: {
request_ids: [requestId],
filenames: ['hero.png'],
filename_matched_request_ids: [],
copy_text: `requestIds=${requestId}`
},
matched_log_count: 1,
events: [{ id: 1, at: '2026-05-12T00:00:01.000Z', level: 'info', message: 'ok' }]
}))
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'diagnose-request.mjs',
[
'--client-request-id',
'web-diag-a',
'--manifest',
manifestPath,
'--filename',
'hero.png',
'--output',
outputPath
],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl, GPT_IMAGE_AGENT_TOKEN: 'diag-token' }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.request_count, 2);
assert.deepEqual(
body.requests.map((item) => item.client_request_id),
['web-diag-a', 'web-diag-b']
);
assert.equal(body.requests[0].feedback, null);
assert.equal(body.requests[1].feedback.value, 'needs_revision');
assert.deepEqual(JSON.parse(readFileSync(outputPath, 'utf8')), body);
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /api/agent/capabilities',
'POST /api/agent/page-requests/feedback',
'POST /api/agent/diagnostics/page-requests'
]
);
assert.equal(requests[1].authorization, 'Bearer diag-token');
assert.equal(requests[2].authorization, 'Bearer diag-token');
}
);
} finally {
rmSync(tempRoot, { recursive: true, force: true });
}
});
it('reads Agent state diagnostics by request id and idempotency key without page feedback calls', async () => {
const requests = [];
await withServer(
async (request, response) => {
requests.push({
method: request.method,
url: request.url,
authorization: request.headers.authorization
});
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
endpoints: {
agent_request_diagnostics: '/api/agent/diagnostics/requests/{id}',
agent_request_diagnostics_lookup: '/api/agent/diagnostics/requests'
}
})
);
return;
}
if (request.url === '/api/agent/diagnostics/requests/req_1') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
found: true,
diagnostics: {
request: {
request_id: 'req_1',
idempotency_key: 'idem_req_1',
mode: 'generate',
status: 'succeeded'
},
response: {
image_count: 1,
timing: { elapsed_ms: 61234, server_elapsed_ms: 61000 },
execution: {
transport: 'agent_json',
endpoint: '/api/agent/images/generate',
request_headers: {
user_agent_effective: 'visual-journal/2.2.0',
has_extra_headers: false
}
}
}
}
})
);
return;
}
if (request.url === '/api/agent/diagnostics/requests?idempotency_key=idem_2') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
found: true,
diagnostics: {
request: {
request_id: 'req_2',
idempotency_key: 'idem_2',
mode: 'generate',
status: 'failed'
},
error: {
code: 'upstream_unavailable',
retryable: true,
diagnostics: {
transport_error_kind: 'dns',
cooldown_until: '2026-06-11T12:00:00.000Z',
cooldown_target: {
channel_id: 'channel-a',
request_mode: 'images-non-stream'
}
}
}
}
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'diagnose-request.mjs',
['--agent-request-id', 'req_1', '--idempotency-key', 'idem_2'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl, GPT_IMAGE_AGENT_TOKEN: 'diag-token' }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.billable, false);
assert.equal(body.page_request_count, 0);
assert.equal(body.request_count, 0);
assert.equal(body.agent_request_count, 2);
assert.deepEqual(body.requests, []);
assert.equal(body.agent_requests[0].lookup.type, 'request_id');
assert.equal(body.agent_requests[0].diagnostics.response.timing.elapsed_ms, 61234);
assert.equal(
body.agent_requests[0].diagnostics.response.execution.request_headers.user_agent_effective,
'visual-journal/2.2.0'
);
assert.equal(body.agent_requests[1].lookup.type, 'idempotency_key');
assert.equal(body.agent_requests[1].diagnostics.error.diagnostics.transport_error_kind, 'dns');
assert.equal(
body.agent_requests[1].diagnostics.error.diagnostics.cooldown_target.request_mode,
'images-non-stream'
);
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /api/agent/capabilities',
'GET /api/agent/diagnostics/requests/req_1',
'GET /api/agent/diagnostics/requests?idempotency_key=idem_2'
]
);
assert.equal(requests[1].authorization, 'Bearer diag-token');
assert.equal(requests[2].authorization, 'Bearer diag-token');
}
);
});
it('lets diagnose-request explicit base-url override the environment service URL', async () => {
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ endpoints: {} }));
return;
}
if (request.url === '/api/agent/diagnostics/requests/req_base_url') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ found: false }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'diagnose-request.mjs',
['--base-url', baseUrl, '--agent-request-id', 'req_base_url'],
{ GPT_IMAGE_PLAYGROUND_URL: 'http://127.0.0.1:9' }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.service_base_url, baseUrl);
assert.equal(body.service_base_url_source, 'user_provided');
assert.equal(body.interactive_confirmation_required, false);
assert.equal(body.agent_found, false);
}
);
});
it('reports missing Agent state diagnostics as found false without failing the script', async () => {
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ endpoints: {} }));
return;
}
if (request.url === '/api/agent/diagnostics/requests/missing-request') {
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ found: false }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'diagnose-request.mjs',
['--agent-request-id', 'missing-request'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.equal(body.agent_request_count, 1);
assert.equal(body.agent_found, false);
assert.equal(body.agent_diagnostics, null);
assert.equal(body.agent_requests[0].found, false);
}
);
});
it('fails diagnostics when the feedback batch response violates the contract', async () => {
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
endpoints: {
page_request_feedback_batch: '/api/agent/page-requests/feedback',
page_request_diagnostics_batch: '/api/agent/diagnostics/page-requests',
page_request_diagnostics: '/api/agent/diagnostics/page-requests/{id}'
}
})
);
return;
}
if (request.url === '/api/agent/page-requests/feedback' && request.method === 'POST') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ targets: [{ type: 'page_request', id: 'web-diag' }] }));
return;
}
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ diagnostics: [] }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync('diagnose-request.mjs', ['--client-request-id', 'web-diag'], {
GPT_IMAGE_PLAYGROUND_URL: baseUrl
});
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
assert.match(result.stderr, /批量反馈响应缺少 feedback 数组/);
}
);
});
it('fails diagnostics when the diagnostics batch response violates the contract', async () => {
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
endpoints: {
page_request_feedback_batch: '/api/agent/page-requests/feedback',
page_request_diagnostics_batch: '/api/agent/diagnostics/page-requests',
page_request_diagnostics: '/api/agent/diagnostics/page-requests/{id}'
}
})
);
return;
}
if (request.url === '/api/agent/page-requests/feedback' && request.method === 'POST') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ feedback: [] }));
return;
}
if (request.url === '/api/agent/diagnostics/page-requests' && request.method === 'POST') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ targets: [{ type: 'page_request', id: 'web-diag' }] }));
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync('diagnose-request.mjs', ['--client-request-id', 'web-diag'], {
GPT_IMAGE_PLAYGROUND_URL: baseUrl
});
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
assert.match(result.stderr, /批量诊断响应缺少 diagnostics 数组/);
}
);
});
it('explains empty page request diagnostics with the advertised log retention boundary', async () => {
const retention = {
storage: 'bounded_local_jsonl',
max_entries: 123,
default_max_entries: 300,
min_entries: 100,
max_configured_entries: 5000,
configured_by: 'APP_LOG_MAX_ENTRIES',
persisted_across_process_restart: true,
loss_modes: ['entry_evicted_by_max_entries', 'log_level_filter', 'local_log_file_missing_or_cleared'],
bounded: true,
not_agent_state_backend: true
};
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
endpoints: {
page_request_feedback_batch: '/api/agent/page-requests/feedback',
page_request_diagnostics_batch: '/api/agent/diagnostics/page-requests',
page_request_diagnostics: '/api/agent/diagnostics/page-requests/{id}'
},
page_request_diagnostics: {
supported: true,
source: 'app_log',
retention
}
})
);
return;
}
if (request.url === '/api/agent/page-requests/feedback' && request.method === 'POST') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ feedback: [] }));
return;
}
if (request.url === '/api/agent/diagnostics/page-requests' && request.method === 'POST') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
diagnostics: [
{
client_request_id: 'missing-log',
scope: {
request_ids: ['missing-log'],
filenames: [],
filename_matched_request_ids: [],
copy_text: 'requestIds=missing-log'
},
matched_log_count: 0,
events: []
}
]
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'diagnose-request.mjs',
['--client-request-id', 'missing-log'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.deepEqual(body.diagnostics_retention, retention);
assert.equal(body.found, false);
assert.equal(body.page_request_query_count, 1);
assert.equal(body.page_request_found_count, 0);
assert.equal(body.diagnostics_note.code, 'no_matching_logs_in_retention_window');
assert.equal(body.requests[0].found, false);
assert.equal(body.requests[0].diagnostics_note.code, 'no_matching_logs_in_retention_window');
assert.equal(body.requests[0].diagnostics_note.retention.max_entries, 123);
assert.match(body.requests[0].diagnostics_note.message, /最近 123 条本地应用日志/);
}
);
});
it('uses diagnostics API retention when capabilities omit diagnostics metadata', async () => {
const retention = {
storage: 'bounded_local_jsonl',
max_entries: 456,
default_max_entries: 300,
min_entries: 100,
max_configured_entries: 5000,
configured_by: 'APP_LOG_MAX_ENTRIES',
persisted_across_process_restart: true,
loss_modes: ['entry_evicted_by_max_entries', 'log_level_filter', 'local_log_file_missing_or_cleared'],
bounded: true,
not_agent_state_backend: true
};
await withServer(
async (request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
endpoints: {
page_request_feedback_batch: '/api/agent/page-requests/feedback',
page_request_diagnostics_batch: '/api/agent/diagnostics/page-requests',
page_request_diagnostics: '/api/agent/diagnostics/page-requests/{id}'
}
})
);
return;
}
if (request.url === '/api/agent/page-requests/feedback' && request.method === 'POST') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ feedback: [] }));
return;
}
if (request.url === '/api/agent/diagnostics/page-requests' && request.method === 'POST') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
diagnostics_retention: retention,
diagnostics: [
{
client_request_id: 'api-retention-only',
scope: {
request_ids: ['api-retention-only'],
filenames: [],
filename_matched_request_ids: [],
copy_text: 'requestIds=api-retention-only'
},
matched_log_count: 0,
events: []
}
]
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'diagnose-request.mjs',
['--client-request-id', 'api-retention-only'],
{ GPT_IMAGE_PLAYGROUND_URL: baseUrl }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
const body = JSON.parse(result.stdout);
assert.deepEqual(body.diagnostics_retention, retention);
assert.equal(body.diagnostics_note.retention.max_entries, 456);
assert.equal(body.requests[0].diagnostics_note.retention.max_entries, 456);
assert.match(body.requests[0].diagnostics_note.message, /最近 456 条本地应用日志/);
}
);
});
it('reads the process-local channel health snapshot through the declared Agent capability', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push({
method: request.method,
url: request.url,
authorization: request.headers.authorization
});
if (request.url === '/playground/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
endpoints: {
channel_health_diagnostics: '/api/agent/diagnostics/channel-health'
},
channel_health_diagnostics: {
supported: true,
endpoint: '/api/agent/diagnostics/channel-health',
source: 'in_process_channel_router',
state_scope: 'process_local',
billable: false
}
})
);
return;
}
if (request.url === '/playground/api/agent/diagnostics/channel-health') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
ok: true,
billable: false,
source: 'in_process_channel_router',
state_scope: 'process_local',
state_initialized: true,
snapshot: {
observed_at: 1_000,
channels: [
{
channel_id: 'primary',
credential_count: 1,
healthy_credential_count: 1,
unhealthy_credential_count: 0,
state: 'healthy',
probe_required: false,
credentials: [
{
credential_id: 'primary#0',
state: 'healthy',
probe_required: false,
request_modes: [
{
mode: 'images-non-stream',
state: 'healthy',
probe_required: false
}
]
}
]
}
]
}
})
);
return;
}
response.writeHead(404, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'missing' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync(
'diagnose-channel-health.mjs',
['--base-url', `${baseUrl}/playground`],
{ GPT_IMAGE_AGENT_TOKEN: 'channel-health-token' }
);
assert.equal(result.status, 0);
assert.equal(result.stderr.trim(), '');
assert.doesNotMatch(result.stdout, /channel-health-token/);
const body = JSON.parse(result.stdout);
assert.equal(body.ok, true);
assert.equal(body.billable, false);
assert.equal(body.state_initialized, true);
assert.equal(body.service_base_url, `${baseUrl}/playground`);
assert.equal(body.service_base_url_source, 'user_provided');
assert.equal(body.snapshot.channels[0].channel_id, 'primary');
assert.deepEqual(
requests.map((item) => `${item.method} ${item.url}`),
[
'GET /playground/api/agent/capabilities',
'GET /playground/api/agent/diagnostics/channel-health'
]
);
assert.equal(requests[0].authorization, 'Bearer channel-health-token');
assert.equal(requests[1].authorization, 'Bearer channel-health-token');
}
);
});
it('does not guess a channel health endpoint when capabilities do not declare support', async () => {
await withServer(
(request, response) => {
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ endpoints: {} }));
return;
}
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected request' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync('diagnose-channel-health.mjs', ['--base-url', baseUrl]);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
assert.match(result.stderr, /channel_health_diagnostics/);
}
);
});
it('rejects inconsistent channel health endpoint declarations before reading diagnostics', async () => {
const requests = [];
await withServer(
(request, response) => {
requests.push([request.method, request.url].join(' '));
if (request.url === '/api/agent/capabilities') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(
JSON.stringify({
endpoints: {
channel_health_diagnostics: '/api/agent/diagnostics/other-channel-health'
},
channel_health_diagnostics: {
supported: true,
endpoint: '/api/agent/diagnostics/channel-health',
source: 'in_process_channel_router',
state_scope: 'process_local',
billable: false
}
})
);
return;
}
response.writeHead(500, { 'content-type': 'application/json' });
response.end(JSON.stringify({ error: 'unexpected request' }));
},
async (baseUrl) => {
const result = await runSkillScriptAsync('diagnose-channel-health.mjs', ['--base-url', baseUrl]);
assert.equal(result.status, 1);
assert.equal(result.stdout.trim(), '');
assert.match(result.stderr, /端点声明不一致/);
assert.deepEqual(requests, ['GET /api/agent/capabilities']);
}
);
});
});
describe('Agent endpoint path drift guards', () => {
it('keeps the Skill endpoint registry identical to the server registry', () => {
assert.deepEqual(AGENT_ENDPOINTS, SERVER_AGENT_ENDPOINTS);
});
it('keeps channel health Skill documentation aligned with the Agent-only contract', () => {
const readmeText = readFileSync(join(repoRoot, 'README.md'), 'utf8');
const skillText = readFileSync(join(skillRoot, 'SKILL.md'), 'utf8');
const apiReference = readFileSync(join(skillRoot, 'references/api.md'), 'utf8');
const openApiSource = readFileSync(join(repoRoot, 'src/lib/agent-openapi.ts'), 'utf8');
assert.equal(AGENT_ENDPOINTS.channel_health_diagnostics, '/api/agent/diagnostics/channel-health');
assert.match(readmeText, /\/api\/agent\/diagnostics\/channel-health/);
assert.match(skillText, /diagnose-channel-health\.mjs/);
assert.match(apiReference, /diagnose-channel-health\.mjs/);
assert.match(skillText, /\/api\/agent\/diagnostics\/channel-health/);
assert.match(apiReference, /\/api\/agent\/diagnostics\/channel-health/);
assert.match(readmeText, /不触发上游探测或图片生成/);
assert.match(readmeText, /不替代页面/);
assert.match(readmeText, /runtime-capabilities/);
assert.match(skillText, /不触发上游探测或图片生成/);
assert.match(apiReference, /不触发上游探测或图片生成/);
assert.match(skillText, /state_initialized=false/);
assert.match(apiReference, /state_initialized=false/);
assert.match(skillText, /不替代页面/);
assert.match(skillText, /runtime-capabilities/);
assert.match(apiReference, /不替代页面/);
assert.match(apiReference, /runtime-capabilities/);
assert.match(openApiSource, /AgentChannelHealthDiagnosticsResponse/);
assert.match(openApiSource, /不会触发上游探测或图片生成/);
});
});
function runSkillScript(filename, args, env = {}, options = {}) {
const baseEnv = buildIsolatedSkillScriptEnv({ loadPrivateAgentEnv: options.loadPrivateAgentEnv });
if (options.createCwd) mkdirSync(options.cwd, { recursive: true });
return spawnSync(process.execPath, [join(skillScriptsRoot, filename), ...args], {
cwd: options.cwd || repoRoot,
encoding: 'utf8',
env: { ...baseEnv, ...env },
...(options.input !== undefined ? { input: options.input } : {})
});
}
function buildIsolatedSkillScriptEnv(options = {}) {
const keepNames = ['HOME', 'PATH', 'SystemRoot', 'TEMP', 'TMP', 'TMPDIR', 'USERPROFILE'];
const isolated = {};
for (const name of keepNames) {
if (process.env[name] !== undefined) isolated[name] = process.env[name];
}
if (!options.loadPrivateAgentEnv) isolated.GPT_IMAGE_AGENT_LOAD_ENV_FILE = '0';
return isolated;
}
function listTextFiles(root) {
const result = [];
for (const name of readdirSync(root)) {
const filePath = join(root, name);
const stat = statSync(filePath);
if (stat.isDirectory()) {
result.push(...listTextFiles(filePath));
} else if (/\.(md|mjs|yaml)$/.test(name)) {
result.push(filePath);
}
}
return result;
}
function runSkillScriptAsync(filename, args, env = {}, options = {}) {
return new Promise((resolve) => {
const baseEnv = buildIsolatedSkillScriptEnv({ loadPrivateAgentEnv: options.loadPrivateAgentEnv });
const child = spawn(process.execPath, [join(skillScriptsRoot, filename), ...args], {
cwd: repoRoot,
env: { ...baseEnv, ...env },
stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
let timedOut = false;
const timeout = options.timeoutMs
? setTimeout(() => {
timedOut = true;
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({ status, signal, stdout, stderr, timedOut });
});
});
}
function createDeferred() {
let resolve;
let reject;
const promise = new Promise((promiseResolve, promiseReject) => {
resolve = promiseResolve;
reject = promiseReject;
});
return { promise, resolve, reject };
}
async function waitWithTimeout(promise, timeoutMs, message) {
let timeout;
const timeoutPromise = new Promise((resolve, reject) => {
timeout = setTimeout(() => {
reject(new Error(message));
}, timeoutMs);
});
try {
return await Promise.race([promise, timeoutPromise]);
} finally {
clearTimeout(timeout);
}
}
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 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);
});
}
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function serverOrigin(request) {
return `http://${request.headers.host}`;
}
function fakePngBuffer(width, height) {
const buffer = Buffer.alloc(24);
buffer[0] = 0x89;
buffer.write('PNG', 1, 'ascii');
buffer[4] = 0x0d;
buffer[5] = 0x0a;
buffer[6] = 0x1a;
buffer[7] = 0x0a;
buffer.writeUInt32BE(13, 8);
buffer.write('IHDR', 12, 'ascii');
buffer.writeUInt32BE(width, 16);
buffer.writeUInt32BE(height, 20);
return buffer;
}
function fakePngBase64(width, height) {
return fakePngBuffer(width, height).toString('base64');
}
function validPngBuffer() {
return Buffer.from(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAADUlEQVQImWP4z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg==',
'base64'
);
}