From 4fb66a15e7d1d1164ad81d1a3a57645790f258b0 Mon Sep 17 00:00:00 2001 From: Abdul Wahab <32850166+abdulwahabone@users.noreply.github.com> Date: Tue, 1 Sep 2026 05:10:41 +0500 Subject: [PATCH] Fix prompt embedding for install paths with spaces (#676) Resolves the embed helper with a filesystem path, reports fallback accurately, and adds regression coverage while leaving generated harness output to the post-merge sync.\n\nAI-assisted maintainer repair, review, and validation by Codex under maintainer direction. --- scripts/test-suites.mjs | 3 +- skill/scripts/generate-image.mjs | 7 ++- tests/generate-image-embed.test.mjs | 84 +++++++++++++++++++++++++++++ 3 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 tests/generate-image-embed.test.mjs diff --git a/scripts/test-suites.mjs b/scripts/test-suites.mjs index 58ba4a2e1..bd85187e1 100644 --- a/scripts/test-suites.mjs +++ b/scripts/test-suites.mjs @@ -26,7 +26,7 @@ export const SUITES = { triggers: [ ...COMMON_INFRA_PATTERNS, /^scripts\/(?!benchmark-detector|build-browser-detector|build-extension)/, - /^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|comp-diff|comp-spec|build-phase|font-match|data\/font-index|concept-seed|context|context-signals|critique-storage|design-parser|doctor|hook|impeccable-paths|is-generated|lib\/(artifact-schema|png|raster|image-metrics|font-fingerprint|font-index|hero-checks|composition-catalog|concept-catalog|provider|staleness|staleness-deep|staleness-notice|surface-briefs|target-slug|template-extensions)|pin|surface-brief))/, + /^skill\/(SKILL\.src\.md|agents\/|reference\/|scripts\/(cleanup-deprecated|comp-diff|comp-spec|build-phase|font-match|data\/font-index|concept-seed|generate-image|context|context-signals|critique-storage|design-parser|doctor|hook|impeccable-paths|is-generated|lib\/(artifact-schema|png|raster|image-metrics|font-fingerprint|font-index|hero-checks|composition-catalog|concept-catalog|provider|staleness|staleness-deep|staleness-notice|surface-briefs|target-slug|template-extensions)|pin|surface-brief))/, /^README(\.npm)?\.md$/, /^cli\/bin\//, ], @@ -54,6 +54,7 @@ export const SUITES = { 'tests/ci-test-plan.test.mjs', 'tests/cli-args.test.mjs', 'tests/concept-seed.test.mjs', + 'tests/generate-image-embed.test.mjs', 'tests/comp-diff.test.mjs', 'tests/build-phase.test.mjs', 'tests/font-match.test.mjs', diff --git a/skill/scripts/generate-image.mjs b/skill/scripts/generate-image.mjs index 1bf97ecaf..c6ba5d393 100644 --- a/skill/scripts/generate-image.mjs +++ b/skill/scripts/generate-image.mjs @@ -431,12 +431,15 @@ fs.writeFileSync(out, Buffer.from(b64, 'base64')); // The prompt travels with the asset: embedded in the file itself (EXIF-class // metadata via embed-prompt.mjs) so intent survives copies across harnesses, // plus a sidecar for anything that indexes rather than opens the image. +let embedded = false; try { const { spawnSync } = await import('node:child_process'); - spawnSync(process.execPath, [new URL('./embed-prompt.mjs', import.meta.url).pathname, out, '--prompt', prompt], { stdio: 'ignore' }); + const result = spawnSync(process.execPath, [fileURLToPath(new URL('./embed-prompt.mjs', import.meta.url)), out, '--prompt', prompt], { stdio: 'ignore' }); + embedded = !result.error && result.status === 0; + if (!embedded) console.warn('generate-image: failed to embed prompt in the image'); fs.writeFileSync(`${out}.json`, JSON.stringify({ prompt, createdAt: new Date().toISOString(), tool: 'generate-image.mjs', model: 'gpt-image-2', ...(refs.length ? { refs } : {}) }, null, 2)); } catch { /* embedding is best-effort */ } -console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); prompt embedded + sidecar at ${out}.json`); +console.log(`IMAGE: ${out} (${size}, ${quality}, gpt-image-2, billed to your OpenAI key); ${embedded ? 'prompt embedded + sidecar' : 'sidecar'} at ${out}.json`); if (plateCtx && plateCtx.chroma) { const frac = await keyChroma(out, plateCtx.chroma); console.log(`PLATE-CHROMA keyed ${(frac * 100).toFixed(0)}% of pixels to alpha (${plateCtx.chroma}); place with a plain over the page's own ground, no background on the plate. If the keyed fraction is under 20% the generator ignored the key: regenerate with --no-chroma and use mix-blend-mode: multiply instead.`); diff --git a/tests/generate-image-embed.test.mjs b/tests/generate-image-embed.test.mjs new file mode 100644 index 000000000..bf8a229cb --- /dev/null +++ b/tests/generate-image-embed.test.mjs @@ -0,0 +1,84 @@ +import { describe, it } from 'node:test'; +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const SOURCE_SCRIPT = path.join(ROOT, 'skill', 'scripts', 'generate-image.mjs'); +const EMBED_SCRIPT = path.join(ROOT, 'skill', 'scripts', 'embed-prompt.mjs'); +const PNG_B64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=='; + +function makeSpacedInstall({ withEmbed = true } = {}) { + const parent = mkdtempSync(path.join(tmpdir(), 'gen-img-parent-')); + const installDir = path.join(parent, 'impeccable space'); + mkdirSync(installDir, { recursive: true }); + copyFileSync(SOURCE_SCRIPT, path.join(installDir, 'generate-image.mjs')); + if (withEmbed) copyFileSync(EMBED_SCRIPT, path.join(installDir, 'embed-prompt.mjs')); + const preload = path.join(parent, 'fetch-mock.mjs'); + writeFileSync(preload, `globalThis.fetch = async () => ({ ok: true, json: async () => ({ data: [{ b64_json: '${PNG_B64}' }] }) });\n`); + const cwd = mkdtempSync(path.join(tmpdir(), 'gen-img-cwd-')); + return { parent, installDir, cwd, script: path.join(installDir, 'generate-image.mjs'), preload }; +} + +function runGenerate(install, prompt) { + const out = path.join(install.cwd, 'out.png'); + const env = { ...process.env, OPENAI_API_KEY: 'test-key' }; + delete env.IMPECCABLE_IMAGE_GEN_FAKE; + const result = spawnSync(process.execPath, [ + '--import', pathToFileURL(install.preload).href, + install.script, + '--prompt', prompt, + '--out', out, + ], { + cwd: install.cwd, + encoding: 'utf-8', + env, + }); + return { ...result, out, sidecar: `${out}.json` }; +} + +function cleanup(install) { + rmSync(install.parent, { recursive: true, force: true }); + rmSync(install.cwd, { recursive: true, force: true }); +} + +describe('generate-image embed', () => { + it('embeds prompt when install path contains a space', () => { + const install = makeSpacedInstall({ withEmbed: true }); + try { + const prompt = 'space-path embed test'; + const result = runGenerate(install, prompt); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /prompt embedded/); + assert.ok(existsSync(result.out)); + assert.ok(existsSync(result.sidecar)); + assert.equal(JSON.parse(readFileSync(result.sidecar, 'utf8')).prompt, prompt); + unlinkSync(result.sidecar); + const readBack = spawnSync(process.execPath, [path.join(install.installDir, 'embed-prompt.mjs'), result.out, '--read'], { + encoding: 'utf-8', + }); + assert.equal(readBack.status, 0, readBack.stderr); + assert.equal(readBack.stdout.trim(), prompt); + } finally { + cleanup(install); + } + }); + + it('warns when embed helper is missing but keeps image and sidecar', () => { + const install = makeSpacedInstall({ withEmbed: false }); + try { + const prompt = 'missing helper test'; + const result = runGenerate(install, prompt); + assert.equal(result.status, 0, result.stderr); + assert.doesNotMatch(result.stdout, /prompt embedded/); + assert.match(result.stderr, /failed to embed prompt/); + assert.ok(existsSync(result.out)); + assert.ok(existsSync(result.sidecar)); + } finally { + cleanup(install); + } + }); +});