refactor(fixtures): move image and text mocks

This commit is contained in:
Vincent Koc 2026-04-29 14:19:44 -07:00
parent ba65248dc2
commit 6a2cf98404
No known key found for this signature in database
3 changed files with 299 additions and 249 deletions

77
src/fixtures/images.js Normal file
View File

@ -0,0 +1,77 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import { DEFAULT_IMAGE_MODEL, PLUGIN_ID } from "../constants.js";
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),
);
const KITCHEN_SINK_OFFICE_SHA256 = sha256Hex(KITCHEN_SINK_OFFICE_IMAGE);
const KITCHEN_IMAGE_FIXTURES = [
{
id: "office-lobby-sink",
label: "Kitchen Sink Office",
assetName: KITCHEN_SINK_OFFICE_IMAGE_FILE,
buffer: KITCHEN_SINK_OFFICE_IMAGE,
sha256: KITCHEN_SINK_OFFICE_SHA256,
mimeType: "image/png",
width: 1024,
height: 1024,
description: "office lobby scene with a lobster-costumed figure holding a real sink",
source: "bundled-real-image",
},
];
export function createKitchenSinkImageAsset({
prompt,
jobId,
scenario = "image.generate",
model = DEFAULT_IMAGE_MODEL,
}) {
const fixture = selectKitchenImageFixture(prompt);
const buffer = Buffer.from(fixture.buffer);
const seed = stableHash(`${jobId}:${prompt}:${fixture.id}`);
return {
buffer,
mimeType: fixture.mimeType,
fileName: `${jobId}.png`,
dataUrl: `data:${fixture.mimeType};base64,${buffer.toString("base64")}`,
revisedPrompt: `Kitchen Sink office image fixture: ${prompt}`,
metadata: {
kitchenSink: true,
assetId: fixture.id,
assetName: fixture.assetName,
source: fixture.source,
model,
width: fixture.width,
height: fixture.height,
sizeBytes: buffer.byteLength,
sha256: fixture.sha256,
contentHash: fixture.sha256.slice(0, 16),
seed,
finishReason: "success",
pluginId: PLUGIN_ID,
scenarioId: scenario,
jobId,
prompt,
},
};
}
function selectKitchenImageFixture(_prompt) {
return KITCHEN_IMAGE_FIXTURES[0];
}
function sha256Hex(buffer) {
return createHash("sha256").update(buffer).digest("hex");
}
function stableHash(input) {
let hash = 2166136261;
for (let index = 0; index < input.length; index += 1) {
hash ^= input.charCodeAt(index);
hash = Math.imul(hash, 16777619);
}
return (hash >>> 0).toString(16).padStart(8, "0");
}

205
src/fixtures/text.js Normal file
View File

@ -0,0 +1,205 @@
import {
DEFAULT_IMAGE_MODEL,
DEFAULT_MEDIA_MODEL,
DEFAULT_TEXT_MODEL,
IMAGE_PROVIDER_ID,
TEXT_PROVIDER_ID,
WEB_FETCH_PROVIDER_ID,
WEB_SEARCH_PROVIDER_ID,
} from "../constants.js";
export function kitchenTextProviderConfig() {
return {
baseUrl: "kitchen-sink://local",
apiKey: "kitchen-sink-local-fixture",
auth: "token",
api: "kitchen-sink",
models: [kitchenTextModelDefinition()],
};
}
export function kitchenTextModelDefinition() {
return {
id: DEFAULT_TEXT_MODEL,
name: "Kitchen Sink Text Fixture",
api: "kitchen-sink",
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 8192,
maxTokens: 2048,
description: "Deterministic OpenClaw plugin text-provider fixture.",
};
}
export function createKitchenTextStream(model, context) {
const stream = createAssistantMessageEventStream();
queueMicrotask(() => {
const prompt = extractLastUserPrompt(context);
const text = kitchenTextResponse(prompt);
const message = {
role: "assistant",
content: [{ type: "text", text }],
api: model?.api || "kitchen-sink",
provider: TEXT_PROVIDER_ID,
model: model?.id || DEFAULT_TEXT_MODEL,
usage: estimateUsage(prompt, text),
stopReason: "stop",
timestamp: Date.now(),
};
stream.push({ type: "start", partial: { ...message, content: [] } });
stream.push({ type: "text_start", contentIndex: 0, partial: { ...message, content: [] } });
stream.push({ type: "text_delta", contentIndex: 0, delta: text, partial: message });
stream.push({ type: "text_end", contentIndex: 0, content: text, partial: message });
stream.push({ type: "done", reason: "stop", message });
stream.end(message);
});
return stream;
}
export function kitchenTextResponse(prompt) {
const normalized = normalizePrompt(prompt, "kitchen sink text inference");
if (/\b(image|picture|draw|generate)\b/i.test(normalized)) {
return [
"Kitchen Sink text fixture:",
`prompt="${normalized}"`,
`I would route this to ${IMAGE_PROVIDER_ID}/${DEFAULT_IMAGE_MODEL}, create a queued image job, wait for completion, then return the bundled kitchen_sink_office.png asset with PNG metadata.`,
].join(" ");
}
if (/\b(search|find|lookup|web)\b/i.test(normalized)) {
return [
"Kitchen Sink text fixture:",
`prompt="${normalized}"`,
`I would call ${WEB_SEARCH_PROVIDER_ID} for ranked fixture results and ${WEB_FETCH_PROVIDER_ID} for deterministic document fetches.`,
].join(" ");
}
if (/\b(rate limit|timeout|fail|error)\b/i.test(normalized)) {
return [
"Kitchen Sink text fixture:",
`prompt="${normalized}"`,
"Failure fixtures are available: rate limit returns 429 with retry metadata, timeout returns 504, and fail returns a deterministic provider error.",
].join(" ");
}
return [
"Kitchen Sink text fixture:",
`prompt="${normalized}"`,
"Available realistic surfaces: direct prefix, registered tools, image provider lifecycle, media understanding, web search, web fetch, channel health, hooks, detached tasks, and text provider catalog.",
].join(" ");
}
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: the bundled kitchen_sink_office PNG: an office lobby scene with a lobster-costumed figure holding a real sink.",
].join(" ");
}
export function estimateUsage(prompt = "", text = "") {
const input = estimateTokens(prompt);
const output = estimateTokens(text);
return {
input,
output,
cacheRead: 0,
cacheWrite: 0,
totalTokens: input + output,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
};
}
function createAssistantMessageEventStream() {
const queue = [];
const waiters = [];
let done = false;
let finalResult;
let resolveResult;
const resultPromise = new Promise((resolve) => {
resolveResult = resolve;
});
return {
push(event) {
if (done) {
return;
}
if (event.type === "done" || event.type === "error") {
finalResult = event.type === "done" ? event.message : event.error;
done = true;
resolveResult(finalResult);
}
const waiter = waiters.shift();
if (waiter) {
waiter({ value: event, done: false });
} else {
queue.push(event);
}
},
end(result) {
if (result !== undefined && finalResult === undefined) {
finalResult = result;
resolveResult(result);
}
done = true;
while (waiters.length > 0) {
waiters.shift()({ value: undefined, done: true });
}
},
async *[Symbol.asyncIterator]() {
while (true) {
if (queue.length > 0) {
yield queue.shift();
} else if (done) {
return;
} else {
const next = await new Promise((resolve) => waiters.push(resolve));
if (next.done) {
return;
}
yield next.value;
}
}
},
result() {
return resultPromise;
},
};
}
function extractLastUserPrompt(context) {
const messages = Array.isArray(context?.messages) ? context.messages : [];
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (message?.role !== "user") {
continue;
}
if (typeof message.content === "string") {
return message.content;
}
if (Array.isArray(message.content)) {
const text = message.content
.filter((item) => item?.type === "text" && typeof item.text === "string")
.map((item) => item.text)
.join(" ")
.trim();
if (text) {
return text;
}
}
}
return "kitchen sink text inference";
}
function estimateTokens(text) {
return Math.max(1, Math.ceil(String(text).trim().split(/\s+/).filter(Boolean).length * 1.35));
}
function normalizePrompt(value, fallback) {
const text = String(value ?? "").replace(/\s+/g, " ").trim();
return text || fallback;
}

View File

@ -1,16 +1,13 @@
import { createHash } from "node:crypto";
import { readFileSync } from "node:fs";
import {
CHANNEL_ACCOUNT_ID,
CHANNEL_ID,
COMPACTION_PROVIDER_ID,
DEFAULT_EMBEDDING_MODEL,
DEFAULT_IMAGE_DELAY_MS,
DEFAULT_IMAGE_MODEL,
DEFAULT_MEDIA_MODEL,
DEFAULT_MUSIC_MODEL,
DEFAULT_SPEECH_MODEL,
DEFAULT_TEXT_MODEL,
DEFAULT_VIDEO_MODEL,
IMAGE_PROVIDER_ID,
MEDIA_PROVIDER_ID,
@ -18,13 +15,21 @@ import {
MUSIC_PROVIDER_ID,
PLUGIN_ID,
REALTIME_TRANSCRIPTION_PROVIDER_ID,
REALTIME_VOICE_PROVIDER_ID,
SPEECH_PROVIDER_ID,
TEXT_PROVIDER_ID,
VIDEO_PROVIDER_ID,
WEB_FETCH_PROVIDER_ID,
WEB_SEARCH_PROVIDER_ID,
} from "./constants.js";
import { createKitchenSinkImageAsset } from "./fixtures/images.js";
import {
createKitchenTextStream,
estimateUsage,
kitchenImageDescription,
kitchenTextModelDefinition,
kitchenTextProviderConfig,
kitchenTextResponse,
} from "./fixtures/text.js";
export {
CHANNEL_ACCOUNT_ID,
@ -51,25 +56,14 @@ export {
WEB_FETCH_PROVIDER_ID,
WEB_SEARCH_PROVIDER_ID,
} from "./constants.js";
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),
);
const KITCHEN_SINK_OFFICE_SHA256 = sha256Hex(KITCHEN_SINK_OFFICE_IMAGE);
const KITCHEN_IMAGE_FIXTURES = [
{
id: "office-lobby-sink",
label: "Kitchen Sink Office",
assetName: KITCHEN_SINK_OFFICE_IMAGE_FILE,
buffer: KITCHEN_SINK_OFFICE_IMAGE,
sha256: KITCHEN_SINK_OFFICE_SHA256,
mimeType: "image/png",
width: 1024,
height: 1024,
description: "office lobby scene with a lobster-costumed figure holding a real sink",
source: "bundled-real-image",
},
];
export { createKitchenSinkImageAsset } from "./fixtures/images.js";
export {
createKitchenTextStream,
kitchenImageDescription,
kitchenTextModelDefinition,
kitchenTextProviderConfig,
kitchenTextResponse,
} from "./fixtures/text.js";
export const KITCHEN_HUMAN_SCENARIOS = Object.freeze([
{
@ -329,37 +323,6 @@ export async function runKitchenScenario(runtime, request = {}) {
};
}
export function createKitchenSinkImageAsset({ prompt, jobId, scenario = "image.generate", model = DEFAULT_IMAGE_MODEL }) {
const fixture = selectKitchenImageFixture(prompt);
const buffer = Buffer.from(fixture.buffer);
const seed = stableHash(`${jobId}:${prompt}:${fixture.id}`);
return {
buffer,
mimeType: fixture.mimeType,
fileName: `${jobId}.png`,
dataUrl: `data:${fixture.mimeType};base64,${buffer.toString("base64")}`,
revisedPrompt: `Kitchen Sink office image fixture: ${prompt}`,
metadata: {
kitchenSink: true,
assetId: fixture.id,
assetName: fixture.assetName,
source: fixture.source,
model,
width: fixture.width,
height: fixture.height,
sizeBytes: buffer.byteLength,
sha256: fixture.sha256,
contentHash: fixture.sha256.slice(0, 16),
seed,
finishReason: "success",
pluginId: PLUGIN_ID,
scenarioId: scenario,
jobId,
prompt,
},
};
}
export function shouldHandleKitchenText(text) {
return /^kitchen(?:\s|$)/i.test(String(text ?? "").trim());
}
@ -761,92 +724,6 @@ export function kitchenImageReply(result) {
};
}
export function kitchenTextProviderConfig() {
return {
baseUrl: "kitchen-sink://local",
apiKey: "kitchen-sink-local-fixture",
auth: "token",
api: "kitchen-sink",
models: [kitchenTextModelDefinition()],
};
}
export function kitchenTextModelDefinition() {
return {
id: DEFAULT_TEXT_MODEL,
name: "Kitchen Sink Text Fixture",
api: "kitchen-sink",
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 8192,
maxTokens: 2048,
description: "Deterministic OpenClaw plugin text-provider fixture.",
};
}
export function createKitchenTextStream(model, context) {
const stream = createAssistantMessageEventStream();
queueMicrotask(() => {
const prompt = extractLastUserPrompt(context);
const text = kitchenTextResponse(prompt);
const message = {
role: "assistant",
content: [{ type: "text", text }],
api: model?.api || "kitchen-sink",
provider: TEXT_PROVIDER_ID,
model: model?.id || DEFAULT_TEXT_MODEL,
usage: estimateUsage(prompt, text),
stopReason: "stop",
timestamp: Date.now(),
};
stream.push({ type: "start", partial: { ...message, content: [] } });
stream.push({ type: "text_start", contentIndex: 0, partial: { ...message, content: [] } });
stream.push({ type: "text_delta", contentIndex: 0, delta: text, partial: message });
stream.push({ type: "text_end", contentIndex: 0, content: text, partial: message });
stream.push({ type: "done", reason: "stop", message });
stream.end(message);
});
return stream;
}
export function kitchenTextResponse(prompt) {
const normalized = normalizePrompt(prompt, "kitchen sink text inference");
if (/\b(image|picture|draw|generate)\b/i.test(normalized)) {
return [
"Kitchen Sink text fixture:",
`prompt="${normalized}"`,
`I would route this to ${IMAGE_PROVIDER_ID}/${DEFAULT_IMAGE_MODEL}, create a queued image job, wait for completion, then return the bundled kitchen_sink_office.png asset with PNG metadata.`,
].join(" ");
}
if (/\b(search|find|lookup|web)\b/i.test(normalized)) {
return [
"Kitchen Sink text fixture:",
`prompt="${normalized}"`,
`I would call ${WEB_SEARCH_PROVIDER_ID} for ranked fixture results and ${WEB_FETCH_PROVIDER_ID} for deterministic document fetches.`,
].join(" ");
}
if (/\b(rate limit|timeout|fail|error)\b/i.test(normalized)) {
return [
"Kitchen Sink text fixture:",
`prompt="${normalized}"`,
"Failure fixtures are available: rate limit returns 429 with retry metadata, timeout returns 504, and fail returns a deterministic provider error.",
].join(" ");
}
return [
"Kitchen Sink text fixture:",
`prompt="${normalized}"`,
"Available realistic surfaces: direct prefix, registered tools, image provider lifecycle, media understanding, web search, web fetch, channel health, hooks, detached tasks, and text provider catalog.",
].join(" ");
}
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: the bundled kitchen_sink_office PNG: an office lobby scene with a lobster-costumed figure holding a real sink.",
].join(" ");
}
export function kitchenToolSchema(promptDescription) {
return {
type: "object",
@ -985,111 +862,6 @@ function transitionKitchenJob(job, status, date, patch = {}) {
};
}
function createAssistantMessageEventStream() {
const queue = [];
const waiters = [];
let done = false;
let finalResult;
let resolveResult;
const resultPromise = new Promise((resolve) => {
resolveResult = resolve;
});
return {
push(event) {
if (done) {
return;
}
if (event.type === "done" || event.type === "error") {
finalResult = event.type === "done" ? event.message : event.error;
done = true;
resolveResult(finalResult);
}
const waiter = waiters.shift();
if (waiter) {
waiter({ value: event, done: false });
} else {
queue.push(event);
}
},
end(result) {
if (result !== undefined && finalResult === undefined) {
finalResult = result;
resolveResult(result);
}
done = true;
while (waiters.length > 0) {
waiters.shift()({ value: undefined, done: true });
}
},
async *[Symbol.asyncIterator]() {
while (true) {
if (queue.length > 0) {
yield queue.shift();
} else if (done) {
return;
} else {
const next = await new Promise((resolve) => waiters.push(resolve));
if (next.done) {
return;
}
yield next.value;
}
}
},
result() {
return resultPromise;
},
};
}
function extractLastUserPrompt(context) {
const messages = Array.isArray(context?.messages) ? context.messages : [];
for (let index = messages.length - 1; index >= 0; index -= 1) {
const message = messages[index];
if (message?.role !== "user") {
continue;
}
if (typeof message.content === "string") {
return message.content;
}
if (Array.isArray(message.content)) {
const text = message.content
.filter((item) => item?.type === "text" && typeof item.text === "string")
.map((item) => item.text)
.join(" ")
.trim();
if (text) {
return text;
}
}
}
return "kitchen sink text inference";
}
function estimateUsage(prompt = "", text = "") {
const input = estimateTokens(prompt);
const output = estimateTokens(text);
return {
input,
output,
cacheRead: 0,
cacheWrite: 0,
totalTokens: input + output,
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
total: 0,
},
};
}
function estimateTokens(text) {
return Math.max(1, Math.ceil(String(text).trim().split(/\s+/).filter(Boolean).length * 1.35));
}
function classifyKitchenFailure(prompt) {
const text = String(prompt ?? "").toLowerCase();
if (/\brate[ -]?limit|429|too many requests\b/.test(text)) {
@ -1121,10 +893,6 @@ function classifyKitchenFailure(prompt) {
return undefined;
}
function selectKitchenImageFixture(_prompt) {
return KITCHEN_IMAGE_FIXTURES[0];
}
function mediaJob(kind, id, prompt, scenarioId) {
const createdAt = "2026-04-28T00:00:00.000Z";
return {