test/codex-image-generation.test.js
Rama main · réplica de lectura del repositorio original.
var test = require("node:test");
var assert = require("node:assert");
var fs = require("fs");
var os = require("os");
var path = require("path");
var EventEmitter = require("events");
var codex = require("../lib/yoke/adapters/codex");
var createSDKBridge = require("../lib/sdk-bridge").createSDKBridge;
var attachImage = require("../lib/project-image").attachImage;
var attachHTTP = require("../lib/project-http").attachHTTP;
var createSessionManager = require("../lib/sessions").createSessionManager;
test("Codex image generation normalizes to an ImageGen tool and generated image", function() {
var state = codex.contractTestKit.createEventState("test-model");
var started = codex.contractTestKit.normalizeEvent({
method: "item/started",
params: { item: { id: "image-1", type: "imageGeneration", status: "inProgress" } },
}, state);
var completed = codex.contractTestKit.normalizeEvent({
method: "item/completed",
params: {
item: {
id: "image-1",
type: "imageGeneration",
status: "completed",
result: "cG5n",
revisedPrompt: "A clay workspace",
savedPath: "/tmp/image.png",
transparentBackground: true,
},
},
}, state);
assert.strictEqual(started[0].yokeType, "tool_start");
assert.strictEqual(started[0].toolName, "ImageGen");
assert.strictEqual(started[1].yokeType, "tool_executing");
assert.strictEqual(completed.length, 1);
assert.deepStrictEqual(completed[0], {
yokeType: "generated_image",
toolId: "image-1",
blockId: started[0].blockId,
data: "cG5n",
mediaType: "image/png",
prompt: "A clay workspace",
savedPath: "/tmp/image.png",
transparentBackground: true,
isError: false,
status: "completed",
});
});
test("generated image data is persisted by reference and delivered by URL", function() {
var calls = [];
var saved = [];
var bridge = createSDKBridge({
cwd: process.cwd(),
slug: "demo",
adapter: { vendor: "codex" },
send: function () {},
sessionManager: {
sendAndRecord: function(session, stored, live) { calls.push({ stored: stored, live: live }); },
},
saveImageFile: function(mediaType, data) {
saved.push({ mediaType: mediaType, data: data });
return "generated.png";
},
});
var session = { localId: 1 };
bridge.processSDKMessage(session, {
yokeType: "generated_image",
toolId: "image-1",
data: "data:image/png;base64,cG5n\n",
prompt: "A clay workspace",
});
assert.deepStrictEqual(saved, [{ mediaType: "image/png", data: "cG5n" }]);
assert.strictEqual(calls.length, 2);
assert.deepStrictEqual(calls[0].stored, {
type: "tool_result",
id: "image-1",
content: "Image generated",
is_error: false,
});
assert.deepStrictEqual(calls[1].stored.imageRefs, [{ mediaType: "image/png", file: "generated.png" }]);
assert.strictEqual(calls[1].stored.prompt, "A clay workspace");
assert.strictEqual(calls[1].stored.images, undefined);
assert.strictEqual(calls[1].live.images[0].url, "/p/demo/images/generated.png");
assert.strictEqual(calls[1].live.prompt, "A clay workspace");
assert.strictEqual(JSON.stringify(calls).indexOf("cG5n"), -1);
});
test("generated images can be copied into a safe project path", function() {
var root = fs.mkdtempSync(path.join(os.tmpdir(), "clay-generated-image-save-"));
var projectDir = path.join(root, "project");
var imagesDir = path.join(root, "images");
var fileName = "1700000000000-abcdef1234567890.png";
try {
fs.mkdirSync(projectDir);
fs.mkdirSync(imagesDir);
fs.writeFileSync(path.join(imagesDir, fileName), Buffer.from("image-data"));
var image = attachImage({ cwd: projectDir, slug: "demo", imagesDir: imagesDir });
var saved = image.saveGeneratedImageToProject(fileName, "assets/generated/hero.png", false, null);
assert.deepStrictEqual(saved, { ok: true, path: "assets/generated/hero.png" });
assert.deepStrictEqual(fs.readFileSync(path.join(projectDir, "assets/generated/hero.png")), Buffer.from("image-data"));
var conflict = image.saveGeneratedImageToProject(fileName, "assets/generated/hero.png", false, null);
assert.strictEqual(conflict.status, 409);
var replaced = image.saveGeneratedImageToProject(fileName, "assets/generated/hero.png", true, null);
assert.strictEqual(replaced.ok, true);
var traversal = image.saveGeneratedImageToProject(fileName, "../outside.png", false, null);
assert.strictEqual(traversal.status, 403);
var absolute = image.saveGeneratedImageToProject(fileName, path.join(projectDir, "absolute.png"), false, null);
assert.strictEqual(absolute.status, 400);
var wrongExtension = image.saveGeneratedImageToProject(fileName, "assets/generated/hero.jpg", false, null);
assert.strictEqual(wrongExtension.status, 400);
if (process.platform !== "win32") {
var outsideDir = path.join(root, "outside");
fs.mkdirSync(outsideDir);
fs.symlinkSync(outsideDir, path.join(projectDir, "linked"));
var linked = image.saveGeneratedImageToProject(fileName, "linked/nested/hero.png", false, null);
assert.strictEqual(linked.status, 403);
assert.strictEqual(fs.existsSync(path.join(outsideDir, "nested")), false);
}
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test("generated image save route forwards a project-relative destination", async function() {
var request = new EventEmitter();
request.method = "POST";
request._clayUser = { role: "admin" };
var received = null;
var responseStatus = null;
var finishResponse;
var responseDone = new Promise(function(resolve) { finishResponse = resolve; });
var response = {
writeHead: function(status) { responseStatus = status; },
end: function(body) { finishResponse(body); },
};
var handler = attachHTTP({
cwd: process.cwd(),
slug: "demo",
project: "Demo",
sm: { sessions: new Map() },
imagesDir: "/tmp",
osUsers: null,
safePath: function() { return null; },
safeAbsPath: function() { return null; },
getOsUserInfoForReq: function() { return null; },
saveGeneratedImageToProject: function(fileName, targetPath, overwrite) {
received = { fileName: fileName, targetPath: targetPath, overwrite: overwrite };
return { ok: true, path: targetPath };
},
_browserTabList: {},
}).handleHTTP;
var handled = handler(request, response, "/api/generated-image/save");
request.emit("data", JSON.stringify({
fileName: "1700000000000-abcdef1234567890.png",
path: "assets/generated/hero.png",
overwrite: false,
}));
request.emit("end");
var responseBody = JSON.parse(await responseDone);
assert.strictEqual(handled, true);
assert.strictEqual(responseStatus, 200);
assert.deepStrictEqual(received, {
fileName: "1700000000000-abcdef1234567890.png",
targetPath: "assets/generated/hero.png",
overwrite: false,
});
assert.deepStrictEqual(responseBody, { ok: true, path: "assets/generated/hero.png" });
});
test("generated image history references hydrate for replay", function() {
var image = attachImage({ cwd: "/tmp/clay-image-generation-test", slug: "demo" });
var hydrated = image.hydrateImageRefs({
type: "generated_image",
id: "image-1",
imageRefs: [{ mediaType: "image/png", file: "generated.png" }],
});
assert.strictEqual(hydrated.imageRefs, undefined);
assert.deepStrictEqual(hydrated.images, [{
mediaType: "image/png",
url: "/p/demo/images/generated.png",
}]);
});
test("session history stores image references while clients receive hydrated URLs", function() {
var root = fs.mkdtempSync(path.join(os.tmpdir(), "clay-generated-image-session-"));
try {
var sent = [];
var manager = createSessionManager({
cwd: path.join(root, "project"),
sessionsBase: path.join(root, "sessions"),
cliSessionsDir: path.join(root, "cli-sessions"),
send: function(message) { sent.push(message); },
});
var session = manager.createSession({ vendor: "codex" });
sent.length = 0;
var stored = {
type: "generated_image",
imageRefs: [{ mediaType: "image/png", file: "generated.png" }],
};
var live = {
type: "generated_image",
images: [{ mediaType: "image/png", url: "/p/demo/images/generated.png" }],
};
manager.sendAndRecord(session, stored, live);
assert.strictEqual(session.history[0], stored);
assert.strictEqual(session.history[0].images, undefined);
assert.strictEqual(sent[0], live);
assert.strictEqual(sent[0]._ts, stored._ts);
} finally {
fs.rmSync(root, { recursive: true, force: true });
}
});
test("generated image UI includes open, download, project save, and prompt details", function() {
var source = fs.readFileSync(path.join(__dirname, "../lib/public/modules/generated-images.js"), "utf8");
var router = fs.readFileSync(path.join(__dirname, "../lib/public/modules/app-messages.js"), "utf8");
var stylesheet = fs.readFileSync(path.join(__dirname, "../lib/public/style.css"), "utf8");
var server = fs.readFileSync(path.join(__dirname, "../lib/project-http.js"), "utf8");
assert.match(source, /showImageModal\(image\.url\)/);
assert.match(source, /downloadLink\.download/);
assert.match(source, /showGeneratedImageDetails/);
assert.match(source, /Copy prompt/);
assert.match(source, /Save to project/);
assert.match(source, /api\/generated-image\/save/);
assert.match(source, /Replace file/);
assert.match(source, /renderGeneratedImage/);
assert.match(source, /generated-image-row/);
assert.match(source, /renderImageGenerationProgress/);
assert.match(source, /progressRow\.replaceWith\(row\)/);
assert.match(router, /case "generated_image":/);
assert.match(router, /renderImageGenerationProgress\(msg\)/);
assert.match(stylesheet, /css\/generated-images\.css/);
assert.match(server, /urlPath === "\/api\/generated-image\/save"/);
});