dist-mcp / mcp / resultMaterializer.test.js
dist-mcp / mcp / resultMaterializer.test.js
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { readMediaState, writeMediaState } from "./core-bundle.mjs";
import { extractSummary, materializeNewImages, snapshotImageIndices, writeHtmlReport } from "./resultMaterializer.js";
async function withScratchpad(run) {
const scratchpadPath = await fs.mkdtemp(path.join(os.tmpdir(), "generate-image-mcp-materialize-"));
try {
await run(scratchpadPath);
}
finally {
await fs.rm(scratchpadPath, { recursive: true, force: true });
}
}
test("snapshotImageIndices captures existing iN before generation", async () => {
await withScratchpad(async (scratchpadPath) => {
const state = await readMediaState(scratchpadPath);
state.images.push({ i: 1, filename: "image-a-i1.png" });
await writeMediaState(scratchpadPath, state);
const before = await snapshotImageIndices(scratchpadPath);
assert.deepEqual([...before], [1]);
});
});
test("materializeNewImages returns only the iN entries appended after the snapshot, sorted, with video detection", async () => {
await withScratchpad(async (scratchpadPath) => {
const state = await readMediaState(scratchpadPath);
state.images.push({ i: 1, filename: "image-a-i1.png" });
await writeMediaState(scratchpadPath, state);
const before = await snapshotImageIndices(scratchpadPath);
// Simulate handleGenerateImage's own appendImages() call happening after the snapshot.
const afterState = await readMediaState(scratchpadPath);
afterState.images.push({ i: 3, filename: "image-b-i3.png" });
afterState.images.push({ i: 2, filename: "image-c-i2.png" });
await writeMediaState(scratchpadPath, afterState);
// i3 has a sibling .mov file — must be detected as a video result.
await fs.writeFile(path.join(scratchpadPath, "image-b-i3.mov"), "video-bytes");
const results = await materializeNewImages(scratchpadPath, before);
assert.deepEqual(results.map((r) => r.index), [2, 3]);
assert.equal(results[0].isVideo, false);
assert.equal(results[1].isVideo, true);
assert.equal(results[1].videoFilename, "image-b-i3.mov");
});
});
test("writeHtmlReport writes an escaped report file next to the generated images", async () => {
await withScratchpad(async (scratchpadPath) => {
const results = [
{ index: 1, notation: "i1", filename: "image-a-i1.png", absolutePath: path.join(scratchpadPath, "image-a-i1.png"), isVideo: false },
];
const reportPath = await writeHtmlReport(scratchpadPath, "req-1", { tool: "generate_image", prompt: "<script>alert(1)</script>" }, results);
assert.equal(path.dirname(reportPath), scratchpadPath);
const html = await fs.readFile(reportPath, "utf8");
assert.match(html, /image-a-i1\.png/);
assert.doesNotMatch(html, /<script>alert/);
assert.match(html, /<script>/);
});
});
test("extractSummary parses the tool result's last JSON text item", () => {
const result = {
content: [
{ type: "text", text: "Image i1 successfully generated." },
{ type: "text", text: JSON.stringify({ width: 1024, height: 1024, model_used: "z_image_turbo.ckpt" }) },
],
};
assert.deepEqual(extractSummary(result), { width: 1024, height: 1024, model_used: "z_image_turbo.ckpt" });
});
test("extractSummary returns undefined for content that isn't JSON", () => {
assert.equal(extractSummary({ content: [{ type: "text", text: "not json" }] }), undefined);
assert.equal(extractSummary({ content: [] }), undefined);
assert.equal(extractSummary(undefined), undefined);
});
test("writeHtmlReport renders one compact bullet-joined metadata line per result, pulling from the tool's own summary", async () => {
await withScratchpad(async (scratchpadPath) => {
const results = [
{ index: 1, notation: "i1", filename: "image-a-i1.png", absolutePath: path.join(scratchpadPath, "image-a-i1.png"), isVideo: false },
];
const reportPath = await writeHtmlReport(scratchpadPath, "req-1", {
tool: "generate_image",
prompt: "A hyper-realistic fox",
canvas: "v2",
summary: { width: 1024, height: 1024, model_used: "ernie_image_q8p.ckpt", mode_effective: "text2image", quality: "high", canvas_source: "v2", inference_time_ms: 5400 },
}, results);
const html = await fs.readFile(reportPath, "utf8");
assert.match(html, /<strong>Prompt:<\/strong> A hyper-realistic fox • <strong>Size:<\/strong> 1024×1024 • <strong>Model:<\/strong> ernie_image_q8p\.ckpt/);
assert.match(html, /<strong>Origin:<\/strong> Draw Things/);
assert.match(html, /<strong>Render time:<\/strong> 5\.4s/);
});
});
test("Source uses summary.canvas_source (now correctly computed in core/tools.ts for every pool type, not just attachment)", async () => {
await withScratchpad(async (scratchpadPath) => {
const results = [
{ index: 2, notation: "i2", filename: "image-a-i2.png", absolutePath: path.join(scratchpadPath, "image-a-i2.png"), isVideo: false },
];
const reportPath = await writeHtmlReport(scratchpadPath, "req-2", { tool: "upscale", canvas: "i1", summary: { canvas_source: "i1", mode_effective: "image2image" } }, results);
const html = await fs.readFile(reportPath, "utf8");
assert.match(html, /<strong>Source:<\/strong> i1/);
});
});
test("Source falls back to the raw caller canvas when summary.canvas_source is absent", async () => {
await withScratchpad(async (scratchpadPath) => {
const results = [
{ index: 2, notation: "i2", filename: "image-a-i2.png", absolutePath: path.join(scratchpadPath, "image-a-i2.png"), isVideo: false },
];
const reportPath = await writeHtmlReport(scratchpadPath, "req-3", { tool: "upscale", canvas: "p1", summary: {} }, results);
const html = await fs.readFile(reportPath, "utf8");
assert.match(html, /<strong>Source:<\/strong> p1/);
});
});
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { readMediaState, writeMediaState } from "./core-bundle.mjs";
import { extractSummary, materializeNewImages, snapshotImageIndices, writeHtmlReport } from "./resultMaterializer.js";
async function withScratchpad(run) {
const scratchpadPath = await fs.mkdtemp(path.join(os.tmpdir(), "generate-image-mcp-materialize-"));
try {
await run(scratchpadPath);
}
finally {
await fs.rm(scratchpadPath, { recursive: true, force: true });
}
}
test("snapshotImageIndices captures existing iN before generation", async () => {
await withScratchpad(async (scratchpadPath) => {
const state = await readMediaState(scratchpadPath);
state.images.push({ i: 1, filename: "image-a-i1.png" });
await writeMediaState(scratchpadPath, state);
const before = await snapshotImageIndices(scratchpadPath);
assert.deepEqual([...before], [1]);
});
});
test("materializeNewImages returns only the iN entries appended after the snapshot, sorted, with video detection", async () => {
await withScratchpad(async (scratchpadPath) => {
const state = await readMediaState(scratchpadPath);
state.images.push({ i: 1, filename: "image-a-i1.png" });
await writeMediaState(scratchpadPath, state);
const before = await snapshotImageIndices(scratchpadPath);
// Simulate handleGenerateImage's own appendImages() call happening after the snapshot.
const afterState = await readMediaState(scratchpadPath);
afterState.images.push({ i: 3, filename: "image-b-i3.png" });
afterState.images.push({ i: 2, filename: "image-c-i2.png" });
await writeMediaState(scratchpadPath, afterState);
// i3 has a sibling .mov file — must be detected as a video result.
await fs.writeFile(path.join(scratchpadPath, "image-b-i3.mov"), "video-bytes");
const results = await materializeNewImages(scratchpadPath, before);
assert.deepEqual(results.map((r) => r.index), [2, 3]);
assert.equal(results[0].isVideo, false);
assert.equal(results[1].isVideo, true);
assert.equal(results[1].videoFilename, "image-b-i3.mov");
});
});
test("writeHtmlReport writes an escaped report file next to the generated images", async () => {
await withScratchpad(async (scratchpadPath) => {
const results = [
{ index: 1, notation: "i1", filename: "image-a-i1.png", absolutePath: path.join(scratchpadPath, "image-a-i1.png"), isVideo: false },
];
const reportPath = await writeHtmlReport(scratchpadPath, "req-1", { tool: "generate_image", prompt: "<script>alert(1)</script>" }, results);
assert.equal(path.dirname(reportPath), scratchpadPath);
const html = await fs.readFile(reportPath, "utf8");
assert.match(html, /image-a-i1\.png/);
assert.doesNotMatch(html, /<script>alert/);
assert.match(html, /<script>/);
});
});
test("extractSummary parses the tool result's last JSON text item", () => {
const result = {
content: [
{ type: "text", text: "Image i1 successfully generated." },
{ type: "text", text: JSON.stringify({ width: 1024, height: 1024, model_used: "z_image_turbo.ckpt" }) },
],
};
assert.deepEqual(extractSummary(result), { width: 1024, height: 1024, model_used: "z_image_turbo.ckpt" });
});
test("extractSummary returns undefined for content that isn't JSON", () => {
assert.equal(extractSummary({ content: [{ type: "text", text: "not json" }] }), undefined);
assert.equal(extractSummary({ content: [] }), undefined);
assert.equal(extractSummary(undefined), undefined);
});
test("writeHtmlReport renders one compact bullet-joined metadata line per result, pulling from the tool's own summary", async () => {
await withScratchpad(async (scratchpadPath) => {
const results = [
{ index: 1, notation: "i1", filename: "image-a-i1.png", absolutePath: path.join(scratchpadPath, "image-a-i1.png"), isVideo: false },
];
const reportPath = await writeHtmlReport(scratchpadPath, "req-1", {
tool: "generate_image",
prompt: "A hyper-realistic fox",
canvas: "v2",
summary: { width: 1024, height: 1024, model_used: "ernie_image_q8p.ckpt", mode_effective: "text2image", quality: "high", canvas_source: "v2", inference_time_ms: 5400 },
}, results);
const html = await fs.readFile(reportPath, "utf8");
assert.match(html, /<strong>Prompt:<\/strong> A hyper-realistic fox • <strong>Size:<\/strong> 1024×1024 • <strong>Model:<\/strong> ernie_image_q8p\.ckpt/);
assert.match(html, /<strong>Origin:<\/strong> Draw Things/);
assert.match(html, /<strong>Render time:<\/strong> 5\.4s/);
});
});
test("Source uses summary.canvas_source (now correctly computed in core/tools.ts for every pool type, not just attachment)", async () => {
await withScratchpad(async (scratchpadPath) => {
const results = [
{ index: 2, notation: "i2", filename: "image-a-i2.png", absolutePath: path.join(scratchpadPath, "image-a-i2.png"), isVideo: false },
];
const reportPath = await writeHtmlReport(scratchpadPath, "req-2", { tool: "upscale", canvas: "i1", summary: { canvas_source: "i1", mode_effective: "image2image" } }, results);
const html = await fs.readFile(reportPath, "utf8");
assert.match(html, /<strong>Source:<\/strong> i1/);
});
});
test("Source falls back to the raw caller canvas when summary.canvas_source is absent", async () => {
await withScratchpad(async (scratchpadPath) => {
const results = [
{ index: 2, notation: "i2", filename: "image-a-i2.png", absolutePath: path.join(scratchpadPath, "image-a-i2.png"), isVideo: false },
];
const reportPath = await writeHtmlReport(scratchpadPath, "req-3", { tool: "upscale", canvas: "p1", summary: {} }, results);
const html = await fs.readFile(reportPath, "utf8");
assert.match(html, /<strong>Source:<\/strong> p1/);
});
});