Spaces:
Running
Running
| 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)); | |
| } | |
| } | |