feat: use bundled kitchen sink image fixture

This commit is contained in:
Vincent Koc 2026-04-28 19:58:56 -07:00
parent a74467af8b
commit 87d20d901e
No known key found for this signature in database
4 changed files with 27 additions and 38 deletions

View File

@ -31,7 +31,8 @@ It also exposes provider and tool surfaces for live model routing:
- `src/scenarios.js` is the shared deterministic fixture engine used by dry
commands, tools, providers, hooks, channel delivery, and tests.
- `kitchen_sink_image_job` returns a deterministic image job, waits 10 seconds
in real runtime execution, then returns a bundled SVG image payload.
in real runtime execution, then returns the bundled `kitchen_sink_office.png`
image payload.
- `kitchen-sink-image` is a registered image generation provider with aliases
`kitchen`, `kitchen-sink`, and `openclaw-kitchen-sink`.
- `kitchen-sink-media` describes images with deterministic fixture text.

View File

@ -108,9 +108,16 @@ assert.equal(imageResult.route, "provider:image");
assert.equal(imageResult.job.status, "completed");
assert.equal(imageResult.job.pluginId, "openclaw-kitchen-sink-fixture");
assert.equal(imageResult.image.metadata.pluginId, "openclaw-kitchen-sink-fixture");
assert.equal(imageResult.image.mimeType, "image/svg+xml");
assert.ok(imageResult.image.buffer.toString("utf8").includes("Kitchen Sink Fixture"));
assert.ok(imageResult.image.dataUrl.startsWith("data:image/svg+xml;base64,"));
assert.equal(imageResult.image.metadata.assetName, "kitchen_sink_office.png");
assert.equal(imageResult.image.metadata.width, 1024);
assert.equal(imageResult.image.metadata.height, 1024);
assert.equal(imageResult.image.mimeType, "image/png");
assert.equal(imageResult.image.fileName, `${imageResult.job.id}.png`);
assert.deepEqual(
[...imageResult.image.buffer.subarray(0, 8)],
[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a],
);
assert.ok(imageResult.image.dataUrl.startsWith("data:image/png;base64,"));
const scenarioResult = await runKitchenScenario(fastRuntime, {
scenario: "web.fetch",

Binary file not shown.

After

Width:  |  Height:  |  Size: 926 KiB

View File

@ -1,3 +1,5 @@
import { readFileSync } from "node:fs";
export const PLUGIN_ID = "openclaw-kitchen-sink-fixture";
export const IMAGE_PROVIDER_ID = "kitchen-sink-image";
export const MEDIA_PROVIDER_ID = "kitchen-sink-media";
@ -10,6 +12,10 @@ export const DEFAULT_IMAGE_MODEL = "kitchen-sink-image-v1";
export const DEFAULT_MEDIA_MODEL = "kitchen-sink-vision-v1";
export const DEFAULT_TEXT_MODEL = "kitchen-sink-text-v1";
export const DEFAULT_IMAGE_DELAY_MS = 10_000;
const KITCHEN_SINK_OFFICE_IMAGE_FILE = "kitchen_sink_office.png";
const KITCHEN_SINK_OFFICE_IMAGE = readFileSync(
new URL(`./assets/${KITCHEN_SINK_OFFICE_IMAGE_FILE}`, import.meta.url),
);
export function createKitchenScenarioRuntime(options = {}) {
const runtime = {
@ -91,16 +97,18 @@ export async function runKitchenScenario(runtime, request = {}) {
}
export function createKitchenSinkImageAsset({ prompt, jobId, scenario = "image.generate" }) {
const svg = renderKitchenSinkSvg({ prompt, jobId });
const buffer = Buffer.from(svg, "utf8");
const buffer = Buffer.from(KITCHEN_SINK_OFFICE_IMAGE);
return {
buffer,
mimeType: "image/svg+xml",
fileName: `${jobId}.svg`,
dataUrl: `data:image/svg+xml;base64,${buffer.toString("base64")}`,
revisedPrompt: `Kitchen sink fixture image for: ${prompt}`,
mimeType: "image/png",
fileName: `${jobId}.png`,
dataUrl: `data:image/png;base64,${buffer.toString("base64")}`,
revisedPrompt: `Kitchen sink office fixture image for: ${prompt}`,
metadata: {
kitchenSink: true,
assetName: KITCHEN_SINK_OFFICE_IMAGE_FILE,
width: 1024,
height: 1024,
pluginId: PLUGIN_ID,
scenarioId: scenario,
jobId,
@ -314,7 +322,7 @@ export function kitchenImageDescription(prompt, count) {
return [
`Kitchen Sink media fixture described ${count || 1} image${count === 1 ? "" : "s"}.`,
`Prompt: ${normalizePrompt(prompt, "describe kitchen sink image")}.`,
"Visible content: a deterministic sink basin, faucet, job id label, and OpenClaw fixture badge.",
"Visible content: the bundled kitchen_sink_office PNG: an office lobby scene with a lobster-costumed figure holding a real sink.",
].join(" ");
}
@ -413,25 +421,6 @@ function createKitchenJob(kind, prompt, date, delayMs, scenarioId, route) {
};
}
function renderKitchenSinkSvg({ prompt, jobId }) {
const safePrompt = escapeXml(prompt).slice(0, 180);
const safeJobId = escapeXml(jobId);
return `<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
<rect width="1024" height="1024" fill="#f7f3ea"/>
<rect x="96" y="150" width="832" height="724" rx="48" fill="#24313a"/>
<rect x="154" y="248" width="716" height="530" rx="72" fill="#d8e7e5"/>
<rect x="234" y="320" width="556" height="380" rx="54" fill="#f8fbf9"/>
<path d="M342 416h340c38 0 68 30 68 68v66c0 82-66 148-148 148H422c-82 0-148-66-148-148v-66c0-38 30-68 68-68Z" fill="#b7d4d1"/>
<path d="M464 388v-64c0-72 58-130 130-130h78v70h-78c-33 0-60 27-60 60v64h-70Z" fill="#5b707a"/>
<rect x="620" y="190" width="144" height="74" rx="28" fill="#5b707a"/>
<circle cx="512" cy="560" r="46" fill="#24313a"/>
<circle cx="512" cy="560" r="22" fill="#8fa5a3"/>
<text x="512" y="835" text-anchor="middle" font-family="Arial, sans-serif" font-size="44" font-weight="700" fill="#f7f3ea">Kitchen Sink Fixture</text>
<text x="512" y="890" text-anchor="middle" font-family="Arial, sans-serif" font-size="24" fill="#d8e7e5">${safeJobId}</text>
<text x="512" y="940" text-anchor="middle" font-family="Arial, sans-serif" font-size="22" fill="#24313a">${safePrompt}</text>
</svg>`;
}
function createAssistantMessageEventStream() {
const queue = [];
const waiters = [];
@ -645,11 +634,3 @@ function stableHash(input) {
}
return (hash >>> 0).toString(16).padStart(8, "0");
}
function escapeXml(value) {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}