import assert from 'node:assert/strict'; import { spawn, spawnSync } from 'node:child_process'; import { createServer } from 'node:http'; import { fileURLToPath } from 'node:url'; import { join } from 'node:path'; import { describe, it } from 'node:test'; import { parseRetryAfterValue, resolveSameOriginUrl } from '../skills/gpt-image-playground-agent/scripts/lib/script-utils.mjs'; const repoRoot = fileURLToPath(new URL('..', import.meta.url)); const skillScriptsRoot = join(repoRoot, 'skills/gpt-image-playground-agent/scripts'); 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('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 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('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('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('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(result.stderr.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('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/); } ); }); }); function runSkillScript(filename, args, env = {}) { return spawnSync(process.execPath, [join(skillScriptsRoot, filename), ...args], { cwd: repoRoot, encoding: 'utf8', env: { ...process.env, ...env } }); } function runSkillScriptAsync(filename, args, env = {}) { return new Promise((resolve) => { const child = spawn(process.execPath, [join(skillScriptsRoot, filename), ...args], { cwd: repoRoot, env: { ...process.env, ...env }, stdio: ['ignore', 'pipe', 'pipe'] }); let stdout = ''; let stderr = ''; 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) => { resolve({ status, stdout, stderr }); }); }); } 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)); } }