src / mcp / resultMaterializer.test.ts
src / mcp / resultMaterializer.test.ts
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, extractSummaryText, materializeNewImages, snapshotImageIndices, writeHtmlReport } from "./resultMaterializer.js";
async function withScratchpad(run: (scratchpadPath: string) => Promise<void>): Promise<void> {
const scratchpadPath = await fs.mkdtemp(path.join(os.tmpdir(), "process-image-mcp-materialize-"));
try {
await run(scratchpadPath);
} finally {
await fs.rm(scratchpadPath, { recursive: true, force: true });
}
}
test("snapshotImageIndices captures existing iN before a handler runs", async () => {
await withScratchpad(async (scratchpadPath) => {
const state = await readMediaState(scratchpadPath);
state.images.push({ i: 1, filename: "crop-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", async () => {
await withScratchpad(async (scratchpadPath) => {
const before = await snapshotImageIndices(scratchpadPath);
const afterState = await readMediaState(scratchpadPath);
afterState.images.push({ i: 2, filename: "crop-b-i2.png" });
afterState.images.push({ i: 1, filename: "crop-a-i1.png" });
await writeMediaState(scratchpadPath, afterState);
const results = await materializeNewImages(scratchpadPath, before);
assert.deepEqual(results.map((r) => r.index), [1, 2]);
assert.equal(results[0].isVideo, false);
});
});
test("extractSummaryText returns the tool result's last text item verbatim", () => {
const summaryText = JSON.stringify({ originalWidth: 1024, originalHeight: 768 });
const result = {
content: [
{ type: "text", text: "Image i1 cropped." },
{ type: "text", text: summaryText },
],
};
assert.equal(extractSummaryText(result), summaryText);
});
test("extractSummary parses the tool result's last JSON text item", () => {
const result = {
content: [{ type: "text", text: JSON.stringify({ originalWidth: 1024, originalHeight: 768 }) }],
};
assert.deepEqual(extractSummary(result), { originalWidth: 1024, originalHeight: 768 });
});
test("writeHtmlReport writes an escaped report file next to the generated images", async () => {
await withScratchpad(async (scratchpadPath) => {
const results = [{ index: 1, notation: "i1", filename: "crop-a-i1.png", absolutePath: path.join(scratchpadPath, "crop-a-i1.png") }];
const reportPath = await writeHtmlReport(scratchpadPath, "req-1", { tool: "crop", canvas: "<script>alert(1)</script>" }, results);
assert.equal(path.dirname(reportPath), scratchpadPath);
const html = await fs.readFile(reportPath, "utf8");
assert.match(html, /crop-a-i1\.png/);
assert.doesNotMatch(html, /<script>alert/);
assert.match(html, /<script>/);
});
});
test("writeHtmlReport renders crop/mask coordinates, not Prompt/Model/Steps", async () => {
await withScratchpad(async (scratchpadPath) => {
const results = [{ index: 1, notation: "i1", filename: "crop-a-i1.png", absolutePath: path.join(scratchpadPath, "crop-a-i1.png") }];
const reportPath = await writeHtmlReport(
scratchpadPath,
"req-1",
{
tool: "crop",
canvas: "a1",
summary: { originalWidth: 1024, originalHeight: 768, crop: { left: { pct: 10 }, right: { pct: 0 }, top: { pct: 5 }, bottom: { pct: 0 } } },
},
results
);
const html = await fs.readFile(reportPath, "utf8");
assert.match(html, /<strong>Size:<\/strong> 1024×768/);
assert.match(html, /<strong>Crop L\/R\/T\/B:<\/strong> 10% \/ 0% \/ 5% \/ 0%/);
assert.match(html, /<strong>Source:<\/strong> a1/);
assert.doesNotMatch(html, /Prompt|Model|Steps|Origin/);
});
});
test("writeHtmlReport renders zoom-in/inpaint/outpaint results like generate_image (Prompt/Size/Model/.../Origin)", async () => {
await withScratchpad(async (scratchpadPath) => {
const results = [{ index: 2, notation: "i2", filename: "inpaint-a-i2.png", absolutePath: path.join(scratchpadPath, "inpaint-a-i2.png") }];
const reportPath = await writeHtmlReport(
scratchpadPath,
"req-2",
{
tool: "inpaint",
prompt: "a red door",
canvas: "p1",
summary: { width: 1024, height: 1024, model_used: "z_image_turbo.ckpt", mode_effective: "edit", canvas_source: "p1", inference_time_ms: 3200 },
},
results
);
const html = await fs.readFile(reportPath, "utf8");
assert.match(html, /<strong>Prompt:<\/strong> a red door/);
assert.match(html, /<strong>Size:<\/strong> 1024×1024/);
assert.match(html, /<strong>Model:<\/strong> z_image_turbo\.ckpt/);
assert.match(html, /<strong>Source:<\/strong> p1/);
assert.match(html, /<strong>Origin:<\/strong> Draw Things/);
});
});
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, extractSummaryText, materializeNewImages, snapshotImageIndices, writeHtmlReport } from "./resultMaterializer.js";
async function withScratchpad(run: (scratchpadPath: string) => Promise<void>): Promise<void> {
const scratchpadPath = await fs.mkdtemp(path.join(os.tmpdir(), "process-image-mcp-materialize-"));
try {
await run(scratchpadPath);
} finally {
await fs.rm(scratchpadPath, { recursive: true, force: true });
}
}
test("snapshotImageIndices captures existing iN before a handler runs", async () => {
await withScratchpad(async (scratchpadPath) => {
const state = await readMediaState(scratchpadPath);
state.images.push({ i: 1, filename: "crop-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", async () => {
await withScratchpad(async (scratchpadPath) => {
const before = await snapshotImageIndices(scratchpadPath);
const afterState = await readMediaState(scratchpadPath);
afterState.images.push({ i: 2, filename: "crop-b-i2.png" });
afterState.images.push({ i: 1, filename: "crop-a-i1.png" });
await writeMediaState(scratchpadPath, afterState);
const results = await materializeNewImages(scratchpadPath, before);
assert.deepEqual(results.map((r) => r.index), [1, 2]);
assert.equal(results[0].isVideo, false);
});
});
test("extractSummaryText returns the tool result's last text item verbatim", () => {
const summaryText = JSON.stringify({ originalWidth: 1024, originalHeight: 768 });
const result = {
content: [
{ type: "text", text: "Image i1 cropped." },
{ type: "text", text: summaryText },
],
};
assert.equal(extractSummaryText(result), summaryText);
});
test("extractSummary parses the tool result's last JSON text item", () => {
const result = {
content: [{ type: "text", text: JSON.stringify({ originalWidth: 1024, originalHeight: 768 }) }],
};
assert.deepEqual(extractSummary(result), { originalWidth: 1024, originalHeight: 768 });
});
test("writeHtmlReport writes an escaped report file next to the generated images", async () => {
await withScratchpad(async (scratchpadPath) => {
const results = [{ index: 1, notation: "i1", filename: "crop-a-i1.png", absolutePath: path.join(scratchpadPath, "crop-a-i1.png") }];
const reportPath = await writeHtmlReport(scratchpadPath, "req-1", { tool: "crop", canvas: "<script>alert(1)</script>" }, results);
assert.equal(path.dirname(reportPath), scratchpadPath);
const html = await fs.readFile(reportPath, "utf8");
assert.match(html, /crop-a-i1\.png/);
assert.doesNotMatch(html, /<script>alert/);
assert.match(html, /<script>/);
});
});
test("writeHtmlReport renders crop/mask coordinates, not Prompt/Model/Steps", async () => {
await withScratchpad(async (scratchpadPath) => {
const results = [{ index: 1, notation: "i1", filename: "crop-a-i1.png", absolutePath: path.join(scratchpadPath, "crop-a-i1.png") }];
const reportPath = await writeHtmlReport(
scratchpadPath,
"req-1",
{
tool: "crop",
canvas: "a1",
summary: { originalWidth: 1024, originalHeight: 768, crop: { left: { pct: 10 }, right: { pct: 0 }, top: { pct: 5 }, bottom: { pct: 0 } } },
},
results
);
const html = await fs.readFile(reportPath, "utf8");
assert.match(html, /<strong>Size:<\/strong> 1024×768/);
assert.match(html, /<strong>Crop L\/R\/T\/B:<\/strong> 10% \/ 0% \/ 5% \/ 0%/);
assert.match(html, /<strong>Source:<\/strong> a1/);
assert.doesNotMatch(html, /Prompt|Model|Steps|Origin/);
});
});
test("writeHtmlReport renders zoom-in/inpaint/outpaint results like generate_image (Prompt/Size/Model/.../Origin)", async () => {
await withScratchpad(async (scratchpadPath) => {
const results = [{ index: 2, notation: "i2", filename: "inpaint-a-i2.png", absolutePath: path.join(scratchpadPath, "inpaint-a-i2.png") }];
const reportPath = await writeHtmlReport(
scratchpadPath,
"req-2",
{
tool: "inpaint",
prompt: "a red door",
canvas: "p1",
summary: { width: 1024, height: 1024, model_used: "z_image_turbo.ckpt", mode_effective: "edit", canvas_source: "p1", inference_time_ms: 3200 },
},
results
);
const html = await fs.readFile(reportPath, "utf8");
assert.match(html, /<strong>Prompt:<\/strong> a red door/);
assert.match(html, /<strong>Size:<\/strong> 1024×1024/);
assert.match(html, /<strong>Model:<\/strong> z_image_turbo\.ckpt/);
assert.match(html, /<strong>Source:<\/strong> p1/);
assert.match(html, /<strong>Origin:<\/strong> Draw Things/);
});
});